从Effects拆分出Events

当操作交互时触发事件处理程序执行。不像事件处理程序,Effects是响应式的,当依赖列表中的依赖(如prop或state变量)发生改变时,Effects响应依赖的改变同步执行Effects。

有时候,你可能希望这两种方式一起混合使用:Effect响应一些值重新运行,而对另外一些值不做响应。这篇文章介绍如何混合使用这两种方式。

事件处理函数和Effects的选择

首先,我们看看时间处理函数和Effects的区别。

假如你正在实现一个聊天室组件,包含有以下2个功能:

  1. 根据下拉框选择的聊天室自动建立连接
  2. 点击"发送"按钮发送聊天信息

事件处理程序响应指定的交互

对用户来说,当用户点击发送按钮时,应该发送消息。如果你在其他时间或因为其他原因发送消息用户会不安。因此,发送消息应该使用事件处理函数:

javascript 复制代码
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');
  // ...
  function handleSendClick() {
    sendMessage(message);
  }
  // ...
  return (
    <>
      <input value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

使用事件处理函数,你可以确保仅仅用户点击发送按钮时sendMessage(message)

需要随时同步时使用Effect

和聊天室建立连接不需要关注界面是怎么操作的,但是建立的连接需要和界面下拉选择的聊天室同步。甚至在初次渲染的时候,用户没有做任何界面操作,仍然需要建立聊天室的连接。所以,建立聊天室连接是一个Effect:

javascript 复制代码
function ChatRoom({ roomId }) {
  // ...
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]);
  // ...
}

不论用户怎么样的界面操作,当roomId改变后,Effect实时同步。

响应式逻辑

事件处理程序需要手动触发,如点击按钮触发。Efefct是自动触发的,当依赖改变时同步Effect。

  • 事件处理程序的逻辑不是响应式的。 除非用户操作某个特定的交互,事件处理程序才会执行。
  • Effects的逻辑是响应式的。 如果重新渲染使依赖的值发生改变,则Effect响应实时同步。

从Effects中抽离出非响应式逻辑

当你把响应式逻辑和非响应式逻辑混合在一起时,事情更棘手。

假如你想在成功建立连接后,在界面展示一个通知。同时你从props获取显示通知的样式主题theme。

javascript 复制代码
function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    // ...

theme是一个响应式的值,所以应该把theme放到依赖列表中:

javascript 复制代码
function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => {
      connection.disconnect()
    };
  }, [roomId, theme]); // ✅ All dependencies declared
  // ...

当roomId改变时,重新建立连接是没有问题的。但是由于theme也是一个依赖,当深色主题和浅色主题切换改变时也会重新建立连接,这不是我们想要的。

也就是说,你不想让下面的代码是响应式的,即使它在Effect中并且是响应式的值:

javascript 复制代码
// ...
showNotification('Connected!', theme);
// ...

你需要找出一种方式从响应式Effect中抽离出非响应式的逻辑。

声明Effect Event

使用一个特殊的HookuseEffectEvent把非响应逻辑从Effect抽离出来。

javascript 复制代码
import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });
  // ...

在这里onConnected叫做Effect Event。它是Effect逻辑的一部分,但是它的行为很像一个事件处理函数。它的逻辑不是响应式的,它读取的是props和state的最新值。

现在我们看看如何在Effect中调用onConnected:

javascript 复制代码
function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...

这样就解决了问题。注意onConnected从依赖列表中移除。Effect Events不是响应式的,不能放到依赖列表中。

Effect Events和事件处理程序很相似的。主要的区别是时间处理程序响应用户的交互,Effect Events在Effects中由你触发。

使用Effect Event读props和state的最新值

例如,假设有一个记录页面访问的Effect:

javascript 复制代码
function Page() {
  useEffect(() => {
    logVisit();
  }, []);
  // ...
}

然后,你向你的网站添加多个路由。现在Page组件接收一个urlprop,这个url表示当前的url路径。代码如下:

javascript 复制代码
function Page({ url }) {
  useEffect(() => {
    logVisit(url);
  }, [url]); // ✅ All dependencies declared
  // ...
}

现在,你想把购物车加入的商品的数量一起发送:

javascript 复制代码
function Page({ url }) {
  const { items } = useContext(ShoppingCartContext);
  const numberOfItems = items.length;

  useEffect(() => {
    logVisit(url, numberOfItems);
  }, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
  // ...
}

在Effect你使用了numberOfItems,linter要求你把它加入依赖列表中。但是当numberOfItems改变时,你不想调用logVisit记录一次访问。当用户把商品放到购物车时,即numberOfItems改变时,并不是说用户又一次访问了页面。

下面把代码分成两部分:

javascript 复制代码
function Page({ url }) {
  const { items } = useContext(ShoppingCartContext);
  const numberOfItems = items.length;

  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, numberOfItems);
  });

  useEffect(() => {
    onVisit(url);
  }, [url]); // ✅ All dependencies declared
  // ...
}

在这里onVisit是一个Effect Event,它不是响应式的。虽然使用了响应式的值numberOfItems,但是numberOfItems改变不会执行Effect Event。

Effect本身是响应式的。Effect使用了urlprop,所以在每一次重新渲染后且url改变了,就会重新执行Effect。在Effect里面的调用onVisit Effect Event。

url改变的时候就会调用logVisit,并且能够读取到numberOfItems的最新值。

Effect Events的限制

在使用Effect Events时,会有限制:

  • 仅仅在Effects内部能调用它
  • 不能把它传递给其他的组件或Hools

例如,不能像下面这样传递Effect Events:

javascript 复制代码
function Timer() {
  const [count, setCount] = useState(0);

  const onTick = useEffectEvent(() => {
    setCount(count + 1);
  });

  useTimer(onTick, 1000); // 🔴 Avoid: Passing Effect Events

  return <h1>{count}</h1>
}

function useTimer(callback, delay) {
  useEffect(() => {
    const id = setInterval(() => {
      callback();
    }, delay);
    return () => {
      clearInterval(id);
    };
  }, [delay, callback]); // Need to specify "callback" in dependencies
}

相反,总是直接在使用它的Effect旁边声明Effect Events:

javascript 复制代码
function Timer() {
  const [count, setCount] = useState(0);
  useTimer(() => {
    setCount(count + 1);
  }, 1000);
  return <h1>{count}</h1>
}

function useTimer(callback, delay) {
  const onTick = useEffectEvent(() => {
    callback();
  });

  useEffect(() => {
    const id = setInterval(() => {
      onTick(); // ✅ Good: Only called locally inside an Effect
    }, delay);
    return () => {
      clearInterval(id);
    };
  }, [delay]); // No need to specify "onTick" (an Effect Event) as a dependency
}

Effect Events是Effect代码非响应式的片段,所以应该在使用它的Effect旁边。

参考文献:

Separating Events from Effects

相关推荐
何时梦醒39 分钟前
第三篇:React 组件化开发 — 函数即组件的哲学与实践
react.js·面试·typescript
sunly_1 小时前
React Suspense 用法详解
前端·javascript·react.js
半个落月18 小时前
React useRef 详解:DOM 引用、持久化值与 Worker 实例
前端·react.js
光影少年21 小时前
React Native网络、存储与原生能力
前端·react native·react.js
To_OC21 小时前
绕开层层 props 搬运:我把 useContext 和自定义 Hook 跑通了
前端·react.js·前端框架
张元清1 天前
React useMount Hook:只在挂载时执行一次的正确姿势 (2026)
javascript·react.js
只一1 天前
React 性能优化精讲:useCallback 与 useMemo 彻底吃透(附实战案例)
前端·react.js
YIAN1 天前
吃透这 5 个核心点,你的 React+TS 代码直接上一个台阶
前端·react.js·typescript
小林ixn1 天前
从混乱到清晰:项目架构与自定义 Hook 的双重实践
react.js·架构·前端框架
breeze jiang1 天前
React + WebGPU 在浏览器运行 DeepSeek:从 Worker 通信到流式生成
前端·javascript·react.js