React:useRef存函数能否在重渲染时保留?无useEffect实现咨询
React响应式副作用挑战:不使用useEffect的实现问题解析
你的代码逻辑漏洞
- 重复绑定事件监听:只要
canMove为true,组件每次重渲染(比如position更新时)都会执行window.addEventListener('pointermove', ref.current),导致同一个事件被绑定多个回调函数。每次渲染时你都会给ref.current赋值新的箭头函数,然后重复添加监听,最终鼠标移动时会多次触发setPosition,引发不必要的重渲染甚至异常。 - 移除监听无效:当
canMove从true切换到false时,你尝试移除ref.current对应的回调,但后续如果再次切换回true,你又会给ref.current赋值新的函数,此时再切换回false,移除的是新函数,而之前绑定的旧函数依然存在于事件监听队列中,无法被清理。 - 渲染阶段执行副作用:React组件的渲染阶段应该是纯函数逻辑,不能包含事件绑定/解绑这类副作用操作。这种写法会导致在React并发渲染模式下出现不可预测的行为,违背了React的设计原则。
不使用useEffect的可行实现方案
可以通过useRef记录当前的canMove状态和回调函数,只在canMove状态真正变化时才执行事件的绑定或解绑,避免重复操作:
const { useState, useRef, Fragment } = React; function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); const callbackRef = useRef(null); const prevCanMoveRef = useRef(canMove); // 存储事件回调 callbackRef.current = (e) => { setPosition({ x: e.clientX, y: e.clientY }); }; // 仅在canMove状态变化时处理事件绑定/解绑 if (prevCanMoveRef.current !== canMove) { if (canMove) { window.addEventListener('pointermove', callbackRef.current); } else { window.removeEventListener('pointermove', callbackRef.current); } prevCanMoveRef.current = canMove; } return ( <Fragment> <label> <input type="checkbox" checked={canMove} onChange={e => setCanMove(e.target.checked)} /> The dot is allowed to move </label> <hr /> <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity: 0.6, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> </Fragment> ); } // Render const container = document.querySelector('#root'); if (container === null) throw new Error('Root container doesn\'t exist'); ReactDOM.createRoot(container).render(<App />);
方案说明
- 用
callbackRef存储事件回调,确保回调的引用在绑定周期内稳定,避免因函数重新创建导致的监听失效。 - 用
prevCanMoveRef记录上一次的canMove状态,只有当状态发生变化时才执行事件的绑定或解绑操作,彻底避免了重复执行副作用的问题。 - 注意:这种写法虽然避开了
useEffect,但依然要严格控制副作用的执行时机,尽量遵循React的渲染逻辑规范。
内容的提问来源于stack exchange,提问作者Indra
相关产品推荐
相关产品推荐

