The code is here: https://codesandbox.io/s/nw4jym4n0
export default ({ name }: Props) => { const [counter, setCounter] = useState(0); useEffect(() => { const interval = setInterval(() => { setCounter(counter + 1); }, 1000); return () => { clearInterval(interval); }; }); return <h1>{counter}</h1>; };
The problem is each setCounter
trigger re-rendering so the interval got reset and re-created. This might looks fine since the state(counter) keeps incrementing, however it could freeze when combining with other hooks.
What's the correct way to do this? In class component it's simple with a instance variable holding the interval.
If you have just made a new project using Create React App or updated to React version 18, you will notice that the useEffect hook is called twice in development mode. This is the case whether you used Create React App or upgraded to React version 18.
twice. Specifically, this component is mounted, then unmounted, and then remounted. This also means that when you fetch data in useEffect, it will be sent twice!
The useEffect callback runs twice for initial render, probably because the component renders twice. After state change the component renders twice but the effect runs once.
The React useMemo Hook returns a memoized value. Think of memoization as caching a value so that it does not need to be recalculated. The useMemo Hook only runs when one of its dependencies update.
You could give an empty array as second argument to useEffect
so that the function is only run once after the initial render. Because of how closures work, this will make the counter
variable always reference the initial value. You could then use the function version of setCounter
instead to always get the correct value.
Example
const { useState, useEffect } = React; function App() { const [counter, setCounter] = useState(0); useEffect(() => { const interval = setInterval(() => { setCounter(counter => counter + 1); }, 1000); return () => { clearInterval(interval); }; }, []); return <h1>{counter}</h1>; }; ReactDOM.render( <App />, document.getElementById('root') );
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script> <div id="root"></div>
A more versatile approach would be to create a new custom hook that stores the function in a ref
and only creates a new interval if the delay
should change, like Dan Abramov does in his great blog post "Making setInterval Declarative with React Hooks".
Example
const { useState, useEffect, useRef } = React; function useInterval(callback, delay) { const savedCallback = useRef(); // Remember the latest callback. useEffect(() => { savedCallback.current = callback; }, [callback]); // Set up the interval. useEffect(() => { let id = setInterval(() => { savedCallback.current(); }, delay); return () => clearInterval(id); }, [delay]); } function App() { const [counter, setCounter] = useState(0); useInterval(() => { setCounter(counter + 1); }, 1000); return <h1>{counter}</h1>; }; ReactDOM.render( <App />, document.getElementById('root') );
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script> <div id="root"></div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With