React严格模式下useState结合useRef时状态重置问题
问题:React严格模式下useState状态莫名重置为默认值
在Next.js开发模式(默认开启React严格模式)中,发现useState状态会自行重置为默认值,该问题仅在严格模式下复现。
复现代码
const { useEffect, useState, useRef } = React; function useFakeQuery() { const [hookState, setHookState] = useState(null); useEffect(() => { console.log("starting timeout"); const timeout = setTimeout(() => { console.log("timeout completed, returning response"); setHookState("response"); }, 1000); return function cleanup() { console.log("cancelling timeout, no response"); clearTimeout(timeout); } }, [setHookState]); return hookState; } const Example = () => { useEffect(() => { console.log("mount"); return function cleanup() { console.log("unmount"); } }, []); const [myState, setMyState] = useState("default"); const queryResponse = useFakeQuery(); console.log("state is", myState, ", query response is", queryResponse); const loadedRef = useRef(false); if (!loadedRef.current && queryResponse != null) { console.log("query has responded (for the first time) with", queryResponse); loadedRef.current = true; if (myState === "default") { console.log("state was still at default, so setting state to query response:", queryResponse); setMyState(queryResponse); } } return <p>State: {myState}</p>; } const App = () => { return( <React.StrictMode> <Example /> </React.StrictMode> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
现象说明
严格模式下组件会先挂载再卸载,随后重新挂载,这是预期行为。第一次挂载的定时器被取消,第二次挂载的定时器完成后,Example组件将loadedRef设为true,并通过setMyState把myState设为"response"。
之后有一次渲染日志显示state is response , query response is response,符合预期;但紧接着又一次渲染日志显示state is default , query response is response,状态莫名重置为默认值,渲染输出变为"State: default"。
补充信息:
- 若用状态而非ref记录hook是否返回响应,问题会消失,但认为ref是合理实现方式,希望确认是否理解有误。
- 关闭严格模式问题消失,但不想关闭严格模式。
原因分析
这不是React的Bug,而是你在渲染阶段直接修改ref并调用setState的写法违反了React规则,加上严格模式的特殊行为放大了问题:
- 严格模式的双重执行机制:React 18严格模式在开发环境下,会对组件渲染逻辑、useEffect等进行两次执行(包括组件函数本身、useState初始化、ref赋值等),目的是检测不安全的副作用。
- 渲染阶段的不可变性要求:React规定,渲染阶段(组件函数执行过程)不能有副作用,包括直接修改ref、调用setState。你的代码在组件函数执行时直接修改
loadedRef.current = true并调用setMyState,属于渲染阶段的非法操作。 - ref状态的不一致:严格模式下,React会对非法的渲染阶段副作用进行检测,可能会“撤销”这类修改,导致ref状态在后续渲染中出现不一致,最终让setState的操作被抵消,状态回到默认值。
正确写法
把依赖queryResponse的逻辑移到useEffect中——useEffect是React专门处理副作用的钩子,不会受严格模式双重执行的影响:
const Example = () => { useEffect(() => { console.log("mount"); return function cleanup() { console.log("unmount"); } }, []); const [myState, setMyState] = useState("default"); const queryResponse = useFakeQuery(); const loadedRef = useRef(false); console.log("state is", myState, ", query response is", queryResponse); // 将逻辑移至useEffect中处理 useEffect(() => { if (!loadedRef.current && queryResponse != null) { console.log("query has responded (for the first time) with", queryResponse); loadedRef.current = true; if (myState === "default") { console.log("state was still at default, so setting state to query response:", queryResponse); setMyState(queryResponse); } } }, [queryResponse, myState]); return <p>State: {myState}</p>; }
为什么用状态代替ref问题会消失?
因为状态更新由React管理,符合React数据流规则,不会在渲染阶段产生非法副作用,严格模式的双重执行不会影响状态的正确流转。
内容的提问来源于stack exchange,提问作者tremby
相关产品推荐
相关产品推荐

