You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 11:32:39