React: Avoid nondeterministic first render (hack with "mounted" state)

Viewed 169

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.

1 Answers

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.

Related