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
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:
- one is the normal way
- one with a "mounted" render hack
- 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.