6

TL;DR React sometimes renders a loading state and sometimes not, without changes in the UI. This is probably due to batched updates.

I would like to know if the problem below is due to batched updates. If the answer is "yes", I would like to know if there's preferred way to opt-out of batched updates in React to get deterministic render behavior. Go down to "Experiment" if you want to skip the setup.


Setup

See on CodeSandbox

enter image description here

Here's the setup, a chart that takes a long time to render. So long that the render is blocking. There are three different ways to render the chart here:

  1. one is the normal way
  2. one with a "mounted" render hack
  3. one with the same "mounted" render hack, but with an additional setTimeout

Option 2 & 3 both have a small useState to check whether they've been mounted. I do this to show a "Loading" state conditionally:

function ChartWithMountHack({ data }: { data: Data }) {
  // initially not mounted
  const [isMounted, setIsMounted] = useState<boolean>(false);

  useEffect(() => {
    // "Now I've been mounted!"
    setIsMounted(true);
  }, []);

  return !isMounted ? <p>Loading</p> : <Chart data={data} />;
}

I did this, because I want to show a "Loading" state instead of a blocking render, so e.g. page switches or ternary rendering (e.g. hasData ? <p>No data</p> : <Chart />) are shown immediately, instead of blocking. (If there are better ways, please let me know!)


Experiment

Now, each button will render one of the three options/charts. Again, the second and third chart have a small hack to check whether they're mounted or not.

Try clicking on the first button and the second button back & forth quickly. You will see that sometimes the "Chart with mount hack" will ("correctly") render the "Loading" state, but sometimes it just doesn't render the "Loading" - instead it blocks the render up until the chart is finished rendering (skips the "Loading" state).

I think this is due to the render cycles and whether you get the two updates in one cycle of the batching. (first: isMounted === false -> second: isMounted === true)

I can't really tell how to reproduce this, hence the "nondeterministic" in the title. Sometimes you also have to click on "Regenerate data" and click back & forth after that.


Cross-check

Option 3 ("Chart with mount hack with timeout") ALWAYS gives me the "Loading" state, which is exactly what I want. The only difference to option 2 is using a setTimeout in the useEffect where isMounted is set to true. setTimeout is used here to break out of the update batching.

Is there a better way to opt-out of the batching, so isMounted will always render with its initial value (false)? Using setTimeout here feels like a hack.

Bennett Dams
  • 6,463
  • 5
  • 25
  • 45
  • You might want have a look at React's upcoming concurrent mode. This sounds like a good usecase. – Jonas Wilms Mar 23 '21 at 15:25
  • Might be interesting for you: https://stackoverflow.com/questions/56727477/react-how-does-react-make-sure-that-useeffect-is-called-after-the-browser-has-h – Jonas Wilms Mar 23 '21 at 15:29
  • @JonasWilms This slow rendering example is definitely a great use case for what concurrent mode could be able to solve. But for this question, the slow rendering is just a tool to demonstrate how the rendering is nondeterministic, which has nothing to do with performance, as batching is already done for the native React events (without concurrent mode). See the asterisk at "Automatic batching of multiple setStates": https://reactjs.org/docs/concurrent-mode-adoption.html – Bennett Dams Mar 23 '21 at 15:31

1 Answers1

0

React has concurrent features to handle these sort of things, for example React Suspense tags or you make use of Subscription libraries like Rxjs, which its subscription should be done in the componentDidMount and componentWillUnmount to unsubscribe the data.

Then the isMounted is just a work around for a pending issue, probably from the library you're using or sometimes just your bundler/build tool acting out a bit.

lastly to avoid unnecessary re-render, you can use React memoization of component using React.Memo.

Kindly read more on these.

Ezekiel
  • 671
  • 5
  • 13