Components not always re-rendering when using useGlobal() without an argument.
See original GitHub issueI am encountering an issue where a component is not always re-rendering correctly depending on how I access a state property. For example, if I have a property foo
on my global state or a provider, there are two ways to access it:
“direct” way:
const [foo, setFoo] = useGlobal('foo');
//then use "foo"
“indirect” way:
const [state, setState] = useGlobal();
//then use "state.foo"
Under some circumstances, the “indirect” way won’t cause the component to re-render. And oddly, just by merely adding a declaration of the state property the “direct” way, all of the issues go away, without changing any of the references.
Also, it seems there are some differences in outcome based on whether we are using a dispatcher vs. manually updating the state. But not always… (I know).
Also, it seems that simply updating any regular React vanilla useState
value in the component will cause a re-render, and all of the state updates we were not seeing magically appear.
So, if you’re a bit confused, don’t worry, I was too. I have spent hours narrowing this down, and thankfully I was finally able to repro it very simply in a code sandbox:
https://codesandbox.io/s/reactn-bug-z7ebn
Let me know if you have any questions, but the sandbox lays out all the details.
Matt
P.S. I reported this several months ago, but didn’t have time to isolate it. So, apologies for the extra ticket.
Issue Analytics
- State:
- Created 4 years ago
- Comments:13 (5 by maintainers)
@CharlesStover Hey, I am just glad I was able to help contribute even a tiny amount to a library I have really enjoyed using. Thanks for implementing the fix, and yay for collaboration and many more successful re-renders in the future : )
First, thank you so much for taking the time to investigate this. You have saved me a lot of effort in my otherwise unfortunately busy schedule. I appreciate the deep dive, and I find it to be a remarkable quality of a developer to do such a task for an open source project one didn’t create themselves.
We want to remove update listeners every re-render as well. Here’s why:
In the above example, the component displays either the number of red fish or blue fish, and which it displays is based on the boolean value
isBlue
.Let’s say that
isBlue
istrue
by default. This component will have accessedisBlue
andnumberofBlueFish
, meaning it needs to re-render if either of those values change. It does not need to re-render ifnumberOfRedFish
changes because it is not even being used.If
isBlue
istrue
,numberOfRedFish
can change from1
to100000
, and it just doesn’t matter to this component, because we’re only showing the value ofnumberOfBlueFish
.Now let’s say that
isBlue
is changed tofalse
. The component re-renders because it is listening toisBlue
, but this time it needsisBlue
andnumberOfRedFish
. It does not need to re-render whennumberOfBlueFish
updates, because it is not even using that value.If
isBlue
isfalse
,numberOfBlueFish
can change from1
to100000
, and it just doesn’t matter to this component, because we’re only showing the value ofnumberOfRedFish
.For this reason, all subscriptions to the global state are removed every render cycle, because they may not be used anymore. The newly-used global state properties will be re-subscribed during the next render cycle, as they are accessed.
My gut feeling is telling me it’s some kind of race condition in React, where the component is unsubscribing after render.
If for some reason, the UNSUBSCRIBE SHOULD BE CALLED HERE is happening at a different location, like perhaps after re-render is complete, that would account for this. This may be the case if the cleanup step occurs asynchronously while the re-render occurs synchronously.
In the above example, where cleanup is async and render is synchronous, you’ll find that
alert('unsubscribe');
occurs afterrender
has finished executing.If this is the case, which I don’t know for sure, this almost seems like a bug with React, but it may be by design. I know that
useEffect
is considered somewhat asynchronous in that it fires after the DOM has been updated. There is auseLayoutEffect
which occurs “more” synchronously in that it fires before the DOM has been updated and allows for state changes that trigger multiple re-renders within a single render cycle.My next idea would be to change
useEffect
here touseLayoutEffect
to see if the cleanup step becomes synchronous, executing earlier in the lifecycle, and cleaning up before re-render instead of after the new subscriptions are made.