-
-
Notifications
You must be signed in to change notification settings - Fork 87
Components not always re-rendering when using useGlobal() without an argument. #150
Comments
Is your issue addressed by the last comment in #137 ? |
@CharlesStover No, unfortunately not. I am not using any |
@CharlesStover Any update on this? |
I can verify that it is occurring in your Code Sandbox example (thank you very much for providing!), and I am simply trying to find time to prioritize the fix. In the meantime, I would advise against using |
Alright, thanks! I was going to see if I could figure out the issue. If I get anywhere, I'll make a pull request. For now, the issue can be worked around by importing all the individual state members that you use from the entire state. Just having them in the component makes that "indirect" global state work fine like so: //reference this in my component for everything...
const [state] = ChatState.useGlobal();
//just importing the individual members, makes the "state" work perfectly:
const [messages] = ChatState.useGlobal('messages');
const [isSendingMessage] = ChatState.useGlobal('isSendingMessage');
const [seenStatusMessage] = ChatState.useGlobal('seenStatusMessage');
const [showDetail] = ChatState.useGlobal('showSeenStatus'); |
So, I took a little time to investigate. I ended up in // If this component ever updates or unmounts, remove the force update
// listener.
useEffect((): VoidFunction => removeForceUpdateListener); If am an understanding, the above effect is going to run the cleanup after each update and on unmount. And my hypothesis here is that this is wrong, since we want the listener to only get removed when the component unmounts so that subsequent state changes will execute the So, I added an empty dependency array to the // Return the entire global state.
if (typeof property === 'undefined') {
// If this component ever updates or unmounts, remove the force update
// listener.
useEffect((): VoidFunction => removeForceUpdateListener, []);
//I added an empty dependency array so the listener
//will only remove when the component unmounts
const globalStateSetter = useCallback(
(
newGlobalState: NewGlobalState<G>,
callback: Callback<G> | null = null,
): Promise<G> =>
setGlobal(globalStateManager, newGlobalState, callback),
[],
);
return [
globalStateManager.spyState(forceUpdate),
globalStateSetter,
];
} Now, I made the same change in the build I am using in my project and it seems to have resolved everything. But I am really not sure of unintended consequences as I don't fully comprehend if there's some reason why it should be removed on update and not just on unmount. What I can say is that it seems to be related to the My concern is that perhaps there's a good reason to remove the listener on updates and this is creating some kind of memory leak that adds a bunch of listeners without the correct cleanup. I am curious what you think and if this is a fix or perhaps not quite. |
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: function MyComponent() {
const [global] = useGlobal();
if (global.isBlue) {
return <div>{global.numberOfBlueFish}</div>;
}
return <div>{global.numberOfRedFish}</div>;
} 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 Let's say that If Now let's say that If 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 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 My next idea would be to change |
Thanks for the detailed explanation of the thought behind your design, I think I get it for the most part. And, I read the other ticket before creating this one, where you recommended |
So, in the property specific one, you subscribe and cleanup all in the same effect. This is how we are all taught to use subscriptions in components in the hooks world: useEffect((): VoidFunction => {
globalStateManager.addPropertyListener(property, forceUpdate);
return removeForceUpdateListener;
}); I wonder if you can refactor somehow to put your global listener in the same effect as the cleanup, I bet this would resolve. I am betting the issue is trying to use That being said, one thing I don't fully understand from the source code: Where is the subscription to the global state being done in this? I can see it easily in the code snippet above from the property specific stuff. But I can't quite see it in the global version. |
The global state object is not:
It is:
This is the "magic" of ReactN that automates subscriptions as if it were baked into React itself, without the boilerplate of having to specify to which properties you want to subscribe. Subscriptions are created for each property as it is accessed. The code looks something like In the case of In the case of You are most likely right though, that the unsubscribe function is being called after the new subscriptions are made. This is accurately portrayed by how you noticed that the first re-render works fine (before unsubscription ever occurs) and only after unsubscribing does it stop working. |
I've finally got around to investigating this today. I've added a unit test to reproduce this issue. The code for
Such a large comment to point out the exact issue of I'm going to go ahead and implement your fix to use Thanks a ton for your investigations! |
@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 : ) |
Enjoyed reading this ticket and seeing the collaboration. Thanks for your efforts 👌 |
I 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:
"indirect" way:
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.
The text was updated successfully, but these errors were encountered: