In my post about useDebounce hook I mentioned that it’s safer to update refs inside useLayoutEffect
(or useEffect
) in React Concurrent Mode.
Today I wanted to explain this topic more in-depth by showing an example of wrong usage of useRef
and explaining how to fix it.
What is different in Concurrent Mode?
React’s work could be separated into 2 phases:
- render (react builds VDOM and finds what needs to be updated)
- commit (React updates the dom)
The main difference in Concurrent Mode is that Reacts’ rendering could be done in few steps.
For instance, if you have a long list of components that you need to render, and some event is fired during that process, React could stop its rendering to handle user events.
Because of that, functional components could be invoked multiple times before a commit happens. As a result, not careful usage of useRef
could lead to unexpected bugs.
Let’s look at this example:
const Component = () => {
const countRef = useRef(0);
// side effect
countRef.current++;
const onClick = useCallback(() => {
console.log(countRef.current++);
}, [countRef]);
return <div onClick={onClick}>{/* ... */}</div>;
};
Imagine that this component is rendered without commit (dom updates). The countRef
will update its value, but the updates are not applied yet and our dom is not synchronized with the component state.
Therefore if another click event happens, we should see the same value.
However, because refs share the same reference between renders, onClick
will always use the latest countRef
value, which would be incorrect until React commits changes.
The solution
The solution to this problem is simple - don’t make any side effects in your components render
(function body).
const Component = () => {
const countRef = useRef(0);
- // side effect
- countRef.current++;
+ useLayoutEffect(() => {
+ // side effect
+ countRef.current++;
+ })
const onClick = useCallback(() => {
console.log(countRef.current++);
}, [countRef]);
return <div onClick={onClick}>{/* ... */}</div>;
};
In this situation, we do it inside a useLayoutEffect
, because we need an updated value as soon as the component commits its changes.
And, as we all know, useLayoutEffect
is called synchronously right after the dom updates. So our onClick
won’t have any time to fire with an old value.
However, in some cases, it’s not important to make a side effect ASAP. In these situations, it’d be preferred to use useEffect
rather.
StrictMode
To catch side effects in your components, you could use a [StrictMode
](strict mode link):
-ReactDOM.render(<App/>, document.getElementById('root'));
+ReactDOM.render(
+ <React.StrictMode>
+ <App/>
+ </React.StrictMode>,
+ document.getElementById('root')
+);
It’ll render you components twice on each update with only 1 commit to help you find edge cases in your app.
It will also add a warning for deprecated APIs that won’t work in concurrent mode.
Conclusion
So the main takeaway from this article is that you need to move your refs’ updates from the component body into effects.
If you liked this article - share it with your friends, it helps.
Peace!