使用 useEffectEvent 取消最新值引用:效果事件
了解 React 19.2 中的 useEffectEvent 如何将非响应式逻辑与效应函数分离,消除对 latest-value ref 的依赖,以及何时该钩子并非合适的工具。
许多 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替代。 - 对于每个依赖项,都要判断其变化是否应重新触发该效果;如果不需要,则读取该值的代码属于事件逻辑。
相关阅读
- 选择合适的 React 工具:Derive、Handle、Fetch、Defer 还是 Effect —— 一份关于如何用派生值、事件处理程序、数据层、useTransition、measured useMemo 以及 React 19 API 替代反射式的 useEffect 调用的决策指南。
- 停止使用 useEffect 同步状态:一种更安全的 React 模式 —— 了解为何使用 useEffect 同步派生状态会导致竞态条件及多余的渲染,以及如何用运行时派生和 key 属性来替代它。
- 通过构建一个简易的useState来理解快照、队列和效应 — 通过创建一个简单的useState实例来了解React钩子的运作方式,再利用该模型解释更新队列、过期快照、闭包、效应清理、引用以及批量处理等概念。
- React 19中结合use、useActionState和useOptimistic实现的异步表单 — 了解use()、useActionState、formStatus和useOptimistic如何取代React 19中的手动加载状态与错误提示机制,以及这些钩子各自隐藏的潜在问题。
- 淘汰旧版 JavaScript 依赖项:CRA、Moment、jQuery 及其他 — 哪些过时的 JavaScript 库和工具需要逐步弃用,它们为何会成为负担,有哪些替代品,以及如何防止依赖列表再次出现问题。