前端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"这个需求有了优雅的解决方案。