useEffect和useEffectEvent 都是 React 中处理副作用 (Side Effects) 的工具,但它们的角色完全不同。
简单来说:
useEffect是“触发器”:只要依赖变了,我就重新执行。useEffectEvent是“消音器”:我能读到最新的值,但我绝不会触发useEffect重新执行。
下面我用通俗的例子帮你彻底搞懂。
1. useEffect: 同步数据的“触发器”
作用: 当某些状态(依赖)发生变化时,执行一段代码来让你的组件和外部系统(比如网络、DOM、定时器)保持同步。
核心逻辑: “只要依赖数组里的东西变了,我就重跑一遍。”
场景举例:聊天室连接
假设你要做一个聊天室组件。
- 当
roomId变化时,你需要断开旧房间,连接新房间。 - 当
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: 解决痛点的“消音器”
作用: 它把一部分逻辑剥离出来,这就好比给这部分逻辑装了“消音器”。你可以在这里读取最新的 props 或 state,但它永远不会导致 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 了!
}
现在的行为:
roomId变了 ->useEffect运行 -> 重连聊天室。theme变了 ->useEffect不运行 (因为 theme 不在依赖里,而 onConnected 是稳定的) -> 聊天室不断开。- 但当重连发生时,
onConnected依然能打印出最新的theme。
总结对比
| 特性 | useEffect | useEffectEvent |
|---|---|---|
| 主要目的 | 同步组件与外部系统 | 提取非响应式逻辑 |
| 是否响应变化 | 是 (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 写法官方化、标准化。