问题
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?
回答1:
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>
</>
);
}
来源:https://stackoverflow.com/questions/65489257/react-setinterval-behavior