首页 / 文章 / 使用 useEffectEvent 取消最新值引用:效果事件

使用 useEffectEvent 取消最新值引用:效果事件

了解 React 19.2 中的 useEffectEvent 如何将非响应式逻辑与效应函数分离,消除对 latest-value ref 的依赖,以及何时该钩子并非合适的工具。

917 词

许多 React 项目都包含一个 useRef,它的唯一作用就是保存“最新值”,这样效果函数就不必依赖它。这种做法虽然可行,但却掩盖了代码的意图。React 19.2 将 useEffectEvent 设为稳定版本,从而为这种模式提供了专门的钩子函数。掌握之后,你便能在效果函数中区分响应式逻辑与非响应式逻辑,并知道何时不应使用该钩子。

无法取胜的依赖数组

这种冲突常见于那些长期存在的连接场景:聊天室、WebSocket 数据流、分析会话、订阅服务等。一个 ChatRoom 组件会接收 roomIdtheme 参数。当 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。
  • 减少无意义的重复执行。该效果仅在与其真正同步的值发生变化时才会触发。
  • 更清晰的心智模型。效果意味着“让此系统保持与当前状态的同步”;而 Effect Event 则意味着“当某事发生时执行此操作”。
  • 自 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 替代。
    • 对于每个依赖项,都要判断其变化是否应重新触发该效果;如果不需要,则读取该值的代码属于事件逻辑。
  • 该 API 规模很小,但却为几乎所有 React 开发者在不知该如何命名时遇到的问题提供了名称。
  • 相关阅读