See the below component, with a simple state and a timer.
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(v => v + 1);
}, 1000);
}, []);
return (
<div className="App">
{count}
</div>
);
}
If you run it in broswer, you may find that the state count
is not incremented by 1, but by 2.
I say you may find because this problem occurs based on two other conditions:
- run component in dev mode
- using react strict mode
So why this happens? If you insert a log in the useEffect hook, even if you define the useEffect hook's dependencies as []
, the log would run twice.
So the problem becomes why the useEffect hook run twice. If you try to build the project and run it in production, you may find the problem gone.
So the reason is strict mode. Actually, it is.
According to its doc, react strict mode does a lot of things like warning about legacy API usage, detecting unexpected side effects, etc. In strict mode, React will try to run phase lifecycles more than once to check if any unexpected side effects.
So now we know the problem is, then how to solve it?
Can we just remove the strict mode part to make it work? Sure we can.
So change below code:
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
into this:
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
);
Yes, this could work. But in this way, we will lost all the benefits the strict mode could bring.
Actually, in this kind of problem, the best way to solve it is to listen to what the strict mode really complains.
In the above example, the real is problem is that every time the component renders, a new timer is created. But before the component unmounts, the timer still exists. This is called the expected effect. So the real solution is to provide a clean function to stop the timer when the component unmounts.
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(v => v + 1);
}, 1000);
// add clean function
return () => {
clearInterval(timer);
};
}, []);
return (
<div className="App">
{count}
</div>
);
}
Most of the time, this is the right solution.
But sometimes, clean function is not enough. Sometimes, forget the clean function, the code should just run 1 time.
In this scenario, the right way is just don't run your function in React component.
// run code here, outside react
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
// don't run the code here
}, []);
return (
<div className="App"> </div>
);
}