Z
ZengLiangYi
Back
前端2026-03-053 min read

React 事件订阅的稳定引用问题:从 useEffect 到 useEffectEvent

分析 React 中事件订阅的闭包陷阱,以及 useEffectEvent 如何优雅地解决稳定引用问题。

ReactHooksuseEffectEvent

经典问题

在 React 中,当你在 useEffect 里订阅事件,并且回调需要访问最新的 state/props 时,就会遇到闭包陷阱:

function ChatRoom({ roomId, onMessage }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on("message", (msg) => {
      // onMessage 是过时的引用!
      onMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, onMessage]); // onMessage 变化会断开重连
}

onMessage 每次渲染都是新引用,导致 effect 频繁重新执行,WebSocket 连接不断断开重连。

传统解决方案:useRef

function ChatRoom({ roomId, onMessage }) {
  const onMessageRef = useRef(onMessage);
  onMessageRef.current = onMessage;

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on("message", (msg) => {
      onMessageRef.current(msg); // 始终最新
    });
    return () => connection.disconnect();
  }, [roomId]); // 不依赖 onMessage
}

能用,但模板代码多,且容易忘记更新 ref。

useEffectEvent:正确的抽象

React 引入的 useEffectEvent 专门解决这个问题:

function ChatRoom({ roomId, onMessage }) {
  const onMsg = useEffectEvent((msg) => {
    onMessage(msg); // 始终读取最新的 onMessage
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on("message", onMsg);
    return () => connection.disconnect();
  }, [roomId]); // onMsg 是稳定的,不需要加入依赖
}

核心语义

useEffectEvent 返回一个稳定引用的函数,但函数体内始终能访问最新的 props 和 state。它本质上是:

  • 引用稳定(不会导致 effect 重新执行)
  • 值最新(不会读到过时的闭包值)

与 useCallback 的区别

| 特性 | useCallback | useEffectEvent | |------|--------------|------------------| | 引用稳定性 | 依赖数组不变时稳定 | 始终稳定 | | 值新鲜度 | 依赖数组决定 | 始终最新 | | 可作为 effect 依赖 | 是 | 否(lint 会警告) | | 适用场景 | 子组件 props | effect 内的事件回调 |

总结

useEffectEvent 填补了 React Hooks 中一个长期存在的抽象缺口。它让"effect 的逻辑依赖某个值,但不应该因为这个值变化而重新执行 effect"这个需求有了优雅的解决方案。