React 进阶:如何优雅地分离“响应式”与“非响应式”逻辑?

58 阅读3分钟

useEffectuseEffectEvent 都是 React 中处理副作用 (Side Effects) 的工具,但它们的角色完全不同。

简单来说:

  • useEffect 是“触发器”:只要依赖变了,我就重新执行。
  • useEffectEvent 是“消音器”:我能读到最新的值,但我绝不会触发 useEffect 重新执行。

下面我用通俗的例子帮你彻底搞懂。


1. useEffect: 同步数据的“触发器”

作用: 当某些状态(依赖)发生变化时,执行一段代码来让你的组件和外部系统(比如网络、DOM、定时器)保持同步。

核心逻辑: “只要依赖数组里的东西变了,我就重跑一遍。”

场景举例:聊天室连接

假设你要做一个聊天室组件。

  1. roomId 变化时,你需要断开旧房间,连接新房间。
  2. serverUrl 变化时,你也需要断开重连。
function ChatRoom({ roomId, serverUrl }) {
  useEffect(() => {
    // 1. 连接逻辑
    const connection = createConnection(serverUrl, roomId);
    connection.connect();

    // 2. 清理逻辑 (断开连接)
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]); // ⚠️ 依赖数组:只要这俩有一个变了,就会断开重连
}

这是 useEffect 最标准、最正确的用法。因为 roomId 变了,不重连就是 Bug。


2. 痛点:不需要重连的时候也重连了

现在的需求变了:当连接成功时,我们要打印一条日志,日志里要包含当前的“主题颜色” (theme)。

如果你直接写进 useEffect

function ChatRoom({ roomId, serverUrl, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      // ❌ 问题出在这里:我们需要读取 theme
      console.log('Connected to', roomId, 'with theme', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl, theme]); // ⚠️ 为了读 theme,必须把它加进依赖
}

后果:

用户只是切换了一下深色模式 (theme 变了),useEffect 发现依赖变了,于是聊天室断开了,又重新连接了一次! 😱

这显然是不可接受的。用户换个皮肤,网络怎么能断呢?

这就是 useEffect 的局限性: 任何你在 Effect 里用到的响应式数据,都必须加入依赖数组,从而导致 Effect 重新执行。


3. useEffectEvent: 解决痛点的“消音器”

作用: 它把一部分逻辑剥离出来,这就好比给这部分逻辑装了“消音器”。你可以在这里读取最新的 propsstate,但它永远不会导致 useEffect 重新运行。

(注:这是一个 React 实验性/新特性 API,旨在解决上述问题)

使用 useEffectEvent 修复聊天室:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, serverUrl, theme }) {
  
  // 1. 把“非响应式”的逻辑包在这个 Hook 里
  const onConnected = useEffectEvent(() => {
    // 这里可以放心地读取最新的 theme
    console.log('Connected to', roomId, 'with theme', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      // 2. 在 Effect 内部调用它
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // ✅ 爽!依赖数组里不需要写 theme 了!
}

现在的行为:

  1. roomId 变了 -> useEffect 运行 -> 重连聊天室。
  2. theme 变了 -> useEffect 不运行 (因为 theme 不在依赖里,而 onConnected 是稳定的) -> 聊天室不断开
  3. 但当重连发生时,onConnected 依然能打印出最新theme

总结对比

特性useEffectuseEffectEvent
主要目的同步组件与外部系统提取非响应式逻辑
是否响应变化 (Reactive)。依赖变了就重跑。 (Non-reactive)。永远是最新的,但不触发重跑。
依赖数组必须写依赖数组 []不需要依赖数组
能否读取 State能,但必须把 State 加到依赖里能,且不需要把 State 加到依赖里
一句话比喻看门狗:有人动了东西我就叫。潜望镜:我偷偷看最新的情况,但不惊动看门狗。

💡 现在的最佳实践

如果你在用的 React 版本还没有 useEffectEvent (或者叫 experimental_useEffectEvent),老手们通常用 useRef 来模拟这个效果:

// useRef 模拟 useEffectEvent 的黑魔法
const themeRef = useRef(theme);

// 每次渲染都更新 ref,保证它是最新的
useEffect(() => {
  themeRef.current = theme;
});

useEffect(() => {
  const connection = createConnection(serverUrl, roomId);
  connection.on('connected', () => {
    // 从 ref 里读,不需要加进依赖
    console.log('Theme is', themeRef.current);
  });
  // ...
}, [roomId, serverUrl]); // 依然不需要 theme

useEffectEvent 就是为了把上面这种丑陋的 useRef 写法官方化、标准化。