let updateTimer: number; export function Timer() { const [count, setCount] = React.useState<number>(0); const [messages, setMessages] = React.useState<string[]>([]); const start = () => { updateTimer = setInterval(() => { const m = [...messages]; m.push("called"); setMessages(m); setCount(count + 1); }, 1000); }; const stop = () => { clearInterval(updateTimer); }; return ( <> <div>{count}</div> <button onClick={start}>Start</button> <button onClick={stop}>Stop</button> {messages.map((message, i) => ( <p key={i}>{message}</p> ))} </> ); }
Code Sample: https://codesandbox.io/s/romantic-wing-9yxw8?file=/src/App.tsx
The code has two buttons – Start and Stop.
Start calls a
setInterval
and saves interval id. Timer set to 1 second (1000 ms).Stop calls a
clearInterval
on the interval id.
The interval id is declared outside the component.
The interval callback function increments a counter and appends a called
message to the UI.
When I click on Start, I expect the counter to increment every second and a corresponding called
message appended underneath the buttons.
What actually happens is that on clicking Start, the counter is incremented just once, and so is the called
message.
If I click on Start again, the counter is incremented and subsequently reset back to its previous value.
If I keep clicking on Start, the counter keeps incrementing and resetting back to its previous value.
Can anyone explain this behavior?
Advertisement
Answer
You have closure on count
value inside the interval’s callback.
Therefore after the first state update with value setState(0+1)
, you will have the same count
value call setState(0+1)
that won’t trigger another render.
Use functional updates which uses the previous state value without closures:
setCount((count) => count + 1);
Same reason for messages
:
setMessages(prev => [...prev,"called"]);
const start = () => { // should be a ref intervalId.current = setInterval(() => { setMessages((prev) => [...prev, "called"]); setCount((count) => count + 1); }, 1000); };
Notice for another possible bug
using an outer scope variable instead of useRef
, for this read about useRef
vs variable differences.
For a reference, here is a simple counter toggle example:
function Component() { // use ref for consisent across multiple components // see https://stackoverflow.com/questions/57444154/why-need-useref-to-contain-mutable-variable-but-not-define-variable-outside-the/57444430#57444430 const intervalRef = useRef(); const [counter, setCounter] = useState(0); // simple toggle with reducer const [isCounterOn, toggleCounter] = useReducer((p) => !p, false); // handle toggle useEffect(() => { if (isCounterOn) { intervalRef.current = setInterval(() => { setCounter((prev) => prev + 1); }, 1000); } else { clearInterval(intervalRef.current); } }, [isCounterOn]); // handle unmount useEffect(() => { // move ref value into callback scope // to not lose its value upon unmounting const intervalId = intervalRef.current; return () => { // using clearInterval(intervalRef.current) may lead to error/warning clearInterval(intervalId); }; }, []); return ( <> {counter} <button onClick={toggleCounter}>Toggle</button> </> ); }