question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Components not always re-rendering when using useGlobal() without an argument.

See original GitHub issue

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:

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:closed
  • Created 4 years ago
  • Comments:13 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
m4tthewericcommented, Feb 18, 2020

@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 : )

1reaction
CharlesStovercommented, Feb 15, 2020

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.

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 forceUpdate method and trigger the component to re-render.

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 isBlue.

Let’s say that isBlue is true by default. This component will have accessed isBlue and numberofBlueFish, meaning it needs to re-render if either of those values change. It does not need to re-render if numberOfRedFish changes because it is not even being used.

If isBlue is true, numberOfRedFish can change from 1 to 100000, and it just doesn’t matter to this component, because we’re only showing the value of numberOfBlueFish.

Now let’s say that isBlue is changed to false. The component re-renders because it is listening to isBlue, but this time it needs isBlue and numberOfRedFish. It does not need to re-render when numberOfBlueFish updates, because it is not even using that value.

If isBlue is false, numberOfBlueFish can change from 1 to 100000, and it just doesn’t matter to this component, because we’re only showing the value of numberOfRedFish.

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.

render is called -> subscriptions made -> render is complete
global state changes -> subscriptions dispatched
UNSUBSCRIBE SHOULD BE CALLED HERE
re-render is called -> subscriptions made -> re-render is complete

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.

const cleanup = async () => {
  await new Promise((resolve) => {
    alert('unsubscribe');
    resolve();
  });
};

const render = () => { /* ... */ };

cleanup();
render();

In the above example, where cleanup is async and render is synchronous, you’ll find that alert('unsubscribe'); occurs after render 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 a useLayoutEffect 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 to useLayoutEffect 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

5 Ways to Avoid React Component Re-Renderings
But, if we use the useMemo() Hook, we can avoid component re-rendering if the inputs are the same and save the result in...
Read more >
When does React re-render components?
When the VDOM gets updated, React compares it to to a previous snapshot of the VDOM and then only updates what has changed...
Read more >
why Functional component is not re-rendering even after a ...
The App component never re renders because none of the previous reasons to re render happened. That means that it never passes changed...
Read more >
Just Say No to Excessive Re-Rendering in React
In this article, we will address instances of excessive re-rendering in React applications and demonstrate how to avoid them.
Read more >
React Hooks - Understanding Component Re-renders
When the App component re-renders, its children would re-render irrespective of whether they consume the theme value (as a prop) are not.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found