React中useEffect无限循环原因分析及startTransition相关疑问
首先来看你的代码:
index.jsx
const { startTransition, useEffect, useState } = React; const { createRoot } = ReactDOM; function App() { const [foo, setFoo] = useState([1]); const [bar, setBar] = useState("1"); useEffect(() => { console.log("infinite loop") setBar(() => { console.log("bar"); return "2"; }) }, [foo]); return ( <> <button onClick={() => { startTransition(() => { setFoo((foo)=>{ console.log("transition"); return []; }); }); setFoo((foo) => { console.log("foo"); return []; }); }} > example </button> </> ); } createRoot(document.getElementById("container")).render(<App />);
HTML部分
<div id="container"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.0/umd/react-dom.production.min.js"></script>
无限循环的原因分析
当你点击按钮时,会触发两个setFoo操作:一个是包裹在startTransition里的过渡更新,另一个是直接调用的同步紧急更新。
同步的setFoo会优先执行,把foo从[1]改成[],触发组件重新渲染。此时useEffect的依赖foo发生了变化,所以会执行useEffect回调——里面调用了setBar更新bar状态,这又会触发一次组件重新渲染。
问题就出在这里:在React 18的更新机制下,当存在pending的过渡更新(也就是startTransition里的setFoo)时,每次setBar触发的渲染都会让React重新检查状态依赖,导致useEffect被反复触发(尽管foo看起来没有变化),进而形成无限循环。而因为这个循环一直没停下来,startTransition里的代码根本没机会执行,所以你看不到"transition"的日志。
你提到的两种解决方法也印证了这个逻辑:
- 删除
setBar:没有了额外的状态更新触发渲染,循环自然终止,过渡更新也能正常执行 - 删除
startTransition:没有了pending的过渡更新,setBar触发的渲染不会再反复触发useEffect,循环也就停止了
关于startTransition的简单介绍
startTransition是React 18新增的API,核心作用是标记非紧急更新,让React优先处理紧急更新(比如用户输入、同步状态变更这类会直接影响UI交互的操作),把非紧急更新推迟到紧急更新完成后再处理。
举个常见的场景:在搜索框输入关键词时,输入操作是紧急更新(必须立刻响应,不然用户会觉得卡顿),而根据关键词请求搜索结果并更新列表的操作就是非紧急的——把这个更新放到startTransition里,就能保证输入流畅,同时后台默默加载搜索结果。
用startTransition包裹的更新会被标记为“过渡中”,React会允许这类更新被中断,如果有新的紧急更新进来,会先处理紧急的,避免UI阻塞。
备注:内容来源于stack exchange,提问作者pistol magic

