使用 useEffectEvent 取消最新值引用:效果事件
了解 React 19.2 中的 useEffectEvent 如何将非响应式逻辑与效应函数分离,消除对 latest-value ref 的依赖,以及何时该钩子并非合适的工具。
来源: The Effects Events in React 19.2: Do not Overuse useEffect
许多 React 项目都包含一个 useRef,它的唯一作用就是保存“最新值”,这样效果函数就不必依赖它。这种做法虽然可行,但却掩盖了代码的意图。React 19.2 将 useEffectEvent 设为稳定版本,从而为这种模式提供了专门的钩子函数。掌握之后,你便能在效果函数中区分响应式逻辑与非响应式逻辑,并知道何时不应使用该钩子。
无法取胜的依赖数组
这种冲突常见于那些长期存在的连接场景:聊天室、WebSocket 数据流、分析会话、订阅服务等。一个 ChatRoom 组件会接收 roomId 和 theme 参数。当 roomId 发生变化时,效果函数需要重新建立连接,但它只是在使用后读取 theme 参数来设置提示信息的样式。
在依赖数组中加入theme会导致切换深色模式时无故断开连接;不加的话则会出现lint警告;即便忽略警告,回调函数仍会保留旧渲染过程中捕获的theme值,这正是典型的闭包问题。
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // stale after theme changes
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme shouldn't trigger a reconnect
}
大多数团队采用的解决方案
常见的解决办法是在每次渲染时将theme复制到ref中,然后在回调函数中读取ref.current。虽然可行,但很容易忽略同步步骤,而且代码中也没有明确说明该逻辑是刻意设计为非响应式的。
使用useEffectEvent分离事件逻辑
useEffectEvent会将效果函数中的非响应式部分提取到独立的函数中。该函数始终能获取最新的属性和状态,调用它不会重新运行效果函数,且不会被纳入依赖数组。
下方,通知会进入 onConnected 状态。该效果仅负责建立连接、注册处理函数,以及在清理时断开连接。
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always fresh, never a dependency
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // only roomId matters now
}
现在只剩下 roomId 作为依赖项。theme 并未被隐藏起来,只是被重新分类了。在连接建立时显示提示信息是对某个事件的响应,因此属于事件逻辑而非同步操作。切换主题不会重新建立连接,后续的提示信息仍会使用当前的主题。
从概念上讲,这正是手动使用 ref 手法所实现的功能:React 会确保从效果函数中调用的函数始终指向最新渲染时的闭包,这样一来其意图就更加清晰了。
你能获得的好处
- 不再有过时的闭包。 Effect Event 可以直接读取最新值,无需手动更新 ref。
自 React 18 版本起,React 文档就建议开发者将事件与效果分开处理;19.2 版终于通过一个稳定的钩子实现了这一建议。关于该版本的其余内容,请参阅我们关于 React 19.2 的 Activity、useEffectEvent 和静态渲染 的概述。
在使用之前
useEffectEvent 并不能替代 useEffect,也不是缩小依赖数组的通用方法。
- 仅当值的变化不应重新触发该效果时才使用它。如果需要重新触发,则该值应放入数组中。
- 绝不在渲染过程中调用 Effect Event,而应在效果内部(如订阅回调或定时器)中触发它。
- 应将其限制在组件内部,而非作为属性传递给子组件。
- 如果某个效果的大部分逻辑都集中在 Effect Event 中,那么这些逻辑很可能更适合放在常规的事件处理函数中。
在 19.2 版本之前,该钩子仅存在于实验性构建中,因此使用 19.0 或 19.1 版本的项目必须升级才能使用稳定版的 API。
关键要点
- 所有“将最新值保存在 ref 中”的变通方法都适合用
useEffectEvent替代。 - 对于每个依赖项,都要判断其变化是否应重新触发该效果;如果不需要,则读取该值的代码属于事件逻辑。