const { createRoot } = ReactDOM;
const { StrictMode, useEffect, useState } = React;
function Test() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<h1>Count: {count}</h1>
);
}
const root = createRoot(document.getElementById("root"));
root.render(<StrictMode><Test /></StrictMode>);
body {
font-family: sans-serif;
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
Consider the above snippet proposing a hypothetical situation. The Test component has a useEffect which increments the count by 1. The useEffect has an empty dependency array which means it should only get called on mount. Therefore, the count should be 1. However, the count is 2, when the strict mode is enabled.
This question is drawn from a comment thread that started here.
What is strict mode doing?
In React, strict mode will run effects twice. From React's documentation (emphasis mine):
Cleaning up state?
The documentation says it's to find bugs caused by missing effect cleanup so you might be thinking the following. What is there to clean up in this effect? The effect isn't controlling a non-React widget (example Stack Overflow question) nor subscribing to an event (example Stack Overflow question); it's just updating some state, there's nothing to clean up.
However, there is clean up that can be done. The clean up is undoing the operation performed in the effect. The
useEffectwould look like this:The full working example:
If you're thinking this is strange, I'd say:
useEffectfiring twice when triggering animations. Code excerpt below:When we think of React unmounting a component it's easy to think React just throws away the component and everything that comes with it (like state). However, this is not true. React might unmount a component then remount it with restored state. This might happen in cases like:
If cleanup isn't performed you'll encounter bugs like the one in the question.
A practical test with fast refresh
You can reproduce this yourself relatively easily by doing the following:
Testcomponent and save it.When you view the app, you'll see the component has updated with the change in step 4 however the count has also incremented. You can make multiple changes, saving each time and see the count continue to increment. This is because React unmounted the component then remounted it without losing state. The
setStatein theuseEffectthen operates on this restored state.Now add in the cleanup described earlier and hard refresh the app to restore its initial state. You can perform step 4 again and see the count doesn't increment on every change.
Further reading