英文:
React setState callback runs multiple times for the same update?
问题
我在更新状态时遇到了一个奇怪的问题,这个问题同时出现在 useEffect 和回调函数中。这是 React 18(自动批处理)且严格模式关闭的情况下。
这里有一个演示:CodeSandbox
function App() {
const [state, setState] = useState([]);
const [state2, setState2] = useState(0);
useEffect(() => {
if (state2) {
console.log("effect");
setState(s => {
console.log("effect setState", s);
return [...s, "effect"];
});
}
}, [state2]);
return (
<input
onMouseDown={() => {
setState2(1);
}}
onFocus={() => {
console.log("focus");
setState(s => {
console.log("focus setState", s);
return [...s, "focus"];
});
}}
/>
);
}
在单击输入框时,onMouseDown
在 onFocus
之前运行。我期望它记录如下:
effect
focus
effect setState []
focus setState ['effect']
然而,它却记录如下:
effect
focus
focus setState []
effect setState []
focus setState ['effect']
为什么在 onFocus
中 setState
回调会运行两次?我在 React 文档或 GitHub 上找不到关于这个行为的任何提及,我猜这与并发模式有关?这导致了难以在 Stack Overflow 问题中解释的微妙错误。
英文:
I came across a weird issue when updating a state in both useEffect and a callback. This is React 18 (automatic batching) with strict mode off.
Here's a demo: CodeSandbox
function App() {
const [state, setState] = useState([]);
const [state2, setState2] = useState(0);
useEffect(() => {
if (state2) {
console.log("effect");
setState(s => {
console.log("effect setState", s);
return [...s, "effect"];
});
}
}, [state2]);
return (
<input
onMouseDown={() => {
setState2(1);
}}
onFocus={() => {
console.log("focus");
setState(s => {
console.log("focus setState", s);
return [...s, "focus"];
});
}}
/>
);
}
When clicking the input, onMouseDown
runs before onFocus
. I expect it to log:
effect
focus
effect setState []
focus setState ['effect']
However, it's logging:
effect
focus
focus setState []
effect setState []
focus setState ['effect']
Why is it running the setState
callback twice in onFocus
? I couldn't find any mention of this behavior in the React docs or Github, I'm assuming this is related to concurrent mode? This is causing subtle bugs that's hard to explain in a SO question.
答案1
得分: 1
这里是您的修改后的沙盒,现在包括当前的渲染迭代...
const render = React.useRef(0);
render.current++;
这会与performance.now()
高分辨率时间戳一起记录在日志中。
console.log(render, performance.now(), "<message>", ...);
您可以在控制台中看到结果...
1 2971 "mousedown"
2 2974 "effect" 1
2 2978 "focus"
3 2978 "focus setState" []
4 2982 "effect setState" []
4 2982 "focus setState" (1) ["effect"]
关于批处理,这个提示在这个声明中提到:
React不会在多个有意义的事件之间批处理
我认为您所看到的情况与React使用严格模式时的情况_类似_...
在严格模式下,React将每个更新函数运行两次(但会丢弃第二个结果),以帮助您找出错误。
在这种情况下,不同事件执行得很接近(但不是同时进行),因此为了避免问题,React会丢弃过时渲染的状态更新(#3),然后重新运行批处理队列。
考虑到最终状态与您期望的状态相同,我不明白其中任何问题,尽管这确实是一个有趣的观察。
英文:
Here's your modified sandbox, now including the current render iteration...
const render = React.useRef(0);
render.current++;
which is logged along with a performance.now()
high-resolution timestamp
console.log(render, performance.now(), "<message>", ...);
You can see the results in the console...
1 2971 "mousedown"
2 2974 "effect" 1
2 2978 "focus"
3 2978 "focus setState" []
4 2982 "effect setState" []
4 2982 "focus setState" (1) ["effect"]
The clue is in this statement regarding batching
> React does not batch across multiple intentional events
I think what you're seeing is similar to when React uses strict mode...
> In Strict Mode, React will run each updater function twice (but discard the second result) to help you find mistakes.
In this case, the different events are executing close together (but not simultaneously) so in order to avoid issues, React is discarding the state update for a stale render (#3) and re-running the batch queue.
Given the end state is the same as the one you expect, I fail to see how any of this is a problem though it is certainly an interesting observation.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论