Next.js/React中useEffect回调读取onClick赋值变量为空及重渲染问题
问题根因
这是React函数组件非常典型的**陈旧闭包(Stale Closure)**问题,和Socket、异步逻辑本身无关:
- 你定义的
x是组件函数作用域内的普通变量,每次组件重渲染时,组件函数会重新执行,所有作用域内的变量、内部函数都会重新生成。旧渲染周期中生成的函数,只会捕获它被创建时所在作用域的变量值,拿不到后续渲染周期里更新的变量。 - 你的事件绑定
useEffect依赖数组只有[socketClientId, socket],只有这两个值变化时,才会解绑旧的Socket响应监听、注册捕获了当前作用域最新值的新监听函数。 - 输入框绑定的
onChange会触发setLastName更新状态,每次输入都会让组件重渲染,但此时socketClientId和socket没有变化,Effect不会重新执行,Socket上始终绑定的是上一次Effect执行时生成的旧listenForResponse函数,这个函数捕获的x是当时的初始空值,自然打印不出你后续赋值的'www'。 - 不修改输入框时功能正常,是因为没有触发额外的重渲染,点击按钮执行赋值、dispatch的整个流程中,绑定的监听函数没有被旧的渲染周期“冻结”,能拿到当前作用域下最新的x值。
修复方案
根据你的场景选对应方案即可:
方案1:用useRef存x(推荐,适合不需要触发视图更新的临时值)
Ref对象的.current属性在所有渲染周期中都是同一个引用,任何地方读取都能拿到最新赋值,不会被闭包冻结,也不会因为值变化触发重渲染、重复绑定事件:
// 组件顶部引入useRef后定义 const xRef = useRef(''); const exec = async (type: string) => { xRef.current = 'www'; await dispatch(exampleRoute()); }; useEffect(() => { const listenForResponse = async ({ success, message, data }: { success: any, message: any, data: any }) => { console.log(xRef.current); }; if (socket) { socket.removeAllListeners('response'); socket.on('response', listenForResponse); } // 新增清理逻辑,避免组件卸载/effect重跑时重复绑定事件造成内存泄漏 return () => socket?.removeAllListeners('response'); }, [socketClientId, socket]);
方案2:用useState管理x,补全Effect依赖
如果x的值变化需要联动视图更新,就用state管理,同时把x加到Effect的依赖数组里,确保x变化时会重新绑定监听函数,捕获最新的x值:
const [x, setX] = useState(''); const exec = async (type: string) => { setX('www'); await dispatch(exampleRoute()); }; useEffect(() => { const listenForResponse = async ({ success, message, data }: { success: any, message: any, data: any }) => { console.log(x); }; if (socket) { socket.removeAllListeners('response'); socket.on('response', listenForResponse); } return () => socket?.removeAllListeners('response'); }, [socketClientId, socket, x]); // 补全x依赖
注意:这个方案每次x变化都会执行一次事件解绑+重绑,事件触发频繁的场景下性能不如Ref方案。
内容的提问来源于stack exchange,提问作者Robert Vlasiu
相关产品推荐
相关产品推荐

