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.
In React, strict mode will run effects twice. From React's documentation (emphasis mine):
Strict Mode enables the following development-only behaviors:
- Your components will re-render an extra time to find bugs caused by impure rendering.
- Your components will re-run Effects an extra time to find bugs caused by missing Effect cleanup.
- Your components will be checked for usage of deprecated APIs.
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 useEffect
would look like this:
useEffect(() => {
setCount((prevCount) => prevCount + 1);
return () => setCount(prevCount => prevCount - 1);
}, []);
The full working example:
const { createRoot } = ReactDOM;
const { StrictMode, useEffect, useState } = React;
function Test() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount((prevCount) => prevCount + 1);
return () => 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>
If you're thinking this is strange, I'd say:
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing.
useEffect
firing twice when triggering animations. Code excerpt below:
useEffect(() => { const node = ref.current; node.style.opacity = 1; // Trigger the animation return () => { node.style.opacity = 0; // Reset to the initial value }; }, []);
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.
You can reproduce this yourself relatively easily by doing the following:
Test
component 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 setState
in the useEffect
then 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.