
一句话:
useEffectEvent解决的是一个长期无解的矛盾——「Effect 里需要读到最新的 props/state,但又不想因此重建副作用」;它返回一个「引用永远稳定、内部永远读到最新值、且只能从 Effect 里调用」的函数。
一、这个矛盾到底是什么
// 场景:连接聊天室,并在「收到消息」时记录「当前主题」与「当前消息数」
function ChatRoom({ roomId, theme, messageCount }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
// ⚠️ 这里读到了 theme / messageCount —— 它们是「本次渲染的快照」
showNotification('已连接', theme);
logVisit(roomId, messageCount);
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 依赖只写了 roomId
// ...
}
⭐ 这段代码有两个选择,而两个选择都是错的:
① 依赖只写 [roomId](当前写法)
❌ 闭包陷阱:回调里读到的 theme / messageCount 永远是「首次渲染的值」
→ 切换主题后再连上,通知显示的还是旧主题
→ 消息数永远是 0
② 依赖写全 [roomId, theme, messageCount]
❌ 副作用被反复重建:
· 主题一变 → 断开连接 + 重新连接(用户看到「正在连接…」闪烁)
· 消息数一变 → 又断开重连(消息一多就疯狂重连)
→ 这显然不可接受
⭐ 结论:这不是「写错依赖数组」的问题,而是「依赖数组这个机制本身」
无法表达「我要最新值,但不要因为它重建」这个诉求。
⭐ 为什么「加依赖会导致重建」?—— 因为 Effect 的语义是「同步」:
依赖变了 = 「需要重新与外部系统对齐」= 先清理旧的、再建立新的。
所以「把 theme 写进依赖」的语义就是「主题变化时重新连接」,
而这在业务上毫无意义。
⭐ 所以社区的诉求是:「给我一个『能读最新值、但不参与依赖比较』的东西」。
React 官方的答案是 useEffectEvent(原提案名 useEvent)。
二、useEffectEvent 的语义
import { useEffectEvent } from 'react'; // ⚠️ React 19 中仍是实验特性(canary/experimental)
function ChatRoom({ roomId, theme, messageCount }) {
// ⭐ 把「需要读最新值」的逻辑抽成 Effect Event
const onConnected = useEffectEvent(() => {
showNotification('已连接', theme); // 永远读到最新 theme
logVisit(roomId, messageCount); // 永远读到最新 messageCount
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected); // ⭐ onConnected 不写进依赖(它引用稳定)
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 依赖干净:只有 roomId
// ...
}
⭐⭐ 三条核心规则(必须背下来):
① 【只能从 Effect 里调用】
✅ useEffect / useLayoutEffect / useInsertionEffect 内
❌ 渲染期间、事件处理函数、其它普通函数里
② 【永远不能写进依赖数组】
它「不是响应式的」(not reactive)——
官方明确说:Effect Events 必须永远从 Effect 的依赖里省略。
③ 【永远不要传给其它组件或 Hook】
它不是普通的回调 props——它是「Effect 内部的私有工具」。
传给子组件会破坏「渲染是 props 的纯函数」这个不变量。
⭐ 与「普通函数」的本质区别(一句话抓住):
普通函数:**闭包 = 创建它那次渲染的快照**(所以会 stale)
Effect Event:**闭包 = 调用时的最新值**(所以不会 stale)
它在「语义上」等价于「用 useRef 保存了最新实现的那个函数」,
但它是「官方承认的、lint 认识的、语义正确的」版本。
三、它和 useRef 模式是同一件事
// ✅ 现在就能用的等价实现(社区标准写法,叫 useEvent)
import { useCallback, useRef, useInsertionEffect } from 'react';
export function useEvent(
handler: (...args: Args) => R
): (...args: Args) => R {
const handlerRef = useRef(handler);
// ⭐⭐ 关键细节:用 useInsertionEffect 而不是 useEffect 来更新 ref
useInsertionEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args: Args) => handlerRef.current(...args), []);
}
⭐⭐ 为什么必须用 useInsertionEffect 更新 ref?(这是最容易被忽略的细节)
Effect 的执行顺序:useInsertionEffect(DOM 变更前)→ useLayoutEffect(绘制前)→ useEffect(绘制后)
场景:如果在 useLayoutEffect 里调用 Effect Event
① 若 ref 用 useEffect 更新(绘制后):
调用时 ref.current 还是「上一轮渲染的 handler」→ ⚠️ 读到旧值
② 若 ref 用 useInsertionEffect 更新(更早):
调用时 ref.current 已经是「本轮 render 的 handler」→ ✅ 读到最新值
→ React 官方的 useEffectEvent 内部也是用 insertion effect 更新 ref 的。
→ 这就是「为什么它能让 layout effect 里也拿到最新值」的原因。
// ✅ 完整可运行版:带类型 + 稳定性保证 + 使用示例
import { useCallback, useRef, useInsertionEffect } from 'react';
/** 返回一个「引用稳定 + 永远读到最新值」的函数(Effect Event 的等价实现) */
export function useEvent(
handler: (...args: Args) => R
): (...args: Args) => R {
const handlerRef = useRef(handler);
useInsertionEffect(() => {
handlerRef.current = handler;
});
// ⭐ 依赖为空 → 引用永久稳定;调用时读 ref → 永远是最新实现
return useCallback((...args: Args) => handlerRef.current(...args), []);
}
四、完整实战:一个「三合一」的场景验证
下面的例子把三个典型场景放在一起,用来验证 useEvent 是否真的解决了两难的依赖问题。
// components/RoomPanel.tsx —— 150+ 行完整实现
import { useCallback, useEffect, useRef, useState, useInsertionEffect } from 'react';
/** ============ 1. useEvent(Effect Event 的等价实现) ============ */
function useEvent(handler: (...args: Args) => R): (...args: Args) => R {
const handlerRef = useRef(handler);
useInsertionEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args: Args) => handlerRef.current(...args), []);
}
/** ============ 2. 可动态调整的定时器(读最新回调,不重建) ============ */
function useInterval(callback: () => void, delay: number | null) {
const onTick = useEvent(callback); // ⭐ 回调不进依赖
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => onTick(), delay);
return () => clearInterval(id);
}, [delay, onTick]); // onTick 稳定 → 实际只随 delay 重建
}
/** ============ 3. 事件监听(读最新 state,不重复绑定) ============ */
function useKeyboardShortcut(key: string, onPress: (e: KeyboardEvent) => void) {
const onKeyDown = useEvent(onPress);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key === key && !e.metaKey && !e.ctrlKey) onKeyDown(e);
};
window.addEventListener('keydown', listener);
return () => window.removeEventListener('keydown', listener);
}, [key, onKeyDown]); // ⭐ 只在 key 变化时重新绑定
}
/** ============ 4. WebSocket 订阅(读最新筛选条件,不重连) ============ */
interface RoomMessage {
id: string;
roomId: string;
text: string;
tag: string;
}
function useRoomSubscription(roomId: string, filterTags: string[], onMessage: (m: RoomMessage) => void) {
const handledMessage = useEvent(onMessage);
const [status, setStatus] = useState<'connecting' | 'open' | 'closed'>('connecting');
useEffect(() => {
setStatus('connecting');
const socket = new WebSocket(`wss://example.com/rooms/${roomId}`);
socket.onopen = () => setStatus('open');
socket.onclose = () => setStatus('closed');
socket.onmessage = (event) => {
const msg = JSON.parse(event.data) as RoomMessage;
// ⚠️ 这里用了 filterTags —— 如果写进依赖,筛选一变就会「断开重连」
// 用 Effect Event 后:连接不变,筛选条件永远最新
if (filterTags.length === 0 || filterTags.includes(msg.tag)) {
handledMessage(msg);
}
};
return () => socket.close();
}, [roomId]); // 依赖干净:只随房间切换而重连
return status;
}
/** ============ 5. 把它们组装成一个面板 ============ */
export function RoomPanel({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState([]);
const [filterTags, setFilterTags] = useState([]);
const [seconds, setSeconds] = useState(0);
const [showOnlyMine, setShowOnlyMine] = useState(false);
// ① 消息订阅:连接只随 roomId 变化
const status = useRoomSubscription(roomId, filterTags, (msg) => {
setMessages((prev) => [...prev, msg]);
});
// ② 计时器:每秒 +1(回调里读的是最新实现,定时器不会被重建)
useInterval(() => setSeconds((s) => s + 1), status === 'open' ? 1000 : null);
// ③ 快捷键:Esc 清空筛选(回调读最新 filterTags)
useKeyboardShortcut('Escape', () => {
if (filterTags.length > 0) setFilterTags([]);
else setMessages([]);
});
// ④ 渲染时用「最新状态」计算(不影响上面两个 Effect 的生命周期)
const visible = showOnlyMine ? messages.filter((m) => m.text.includes('我')) : messages;
return (
连接状态:{status}
已连接 {seconds} 秒
{['公告', '闲聊', '求助'].map((tag) => (
))}
{visible.map((m) => (
-
{m.tag} {m.text}
))}
);
}
⭐⭐ 在这个例子里,如果没有 useEvent,会出现什么?
① 消息订阅(filterTags)
写进依赖 → 每次点标签筛选,WebSocket 就「断开 + 重连」(服务端连接数暴涨、消息丢失)
不写依赖 → 筛选永远是「首次渲染的空数组」→ 筛选功能完全失效
② 计时器(setSeconds 里的 s)
这里用的是函数式更新,所以没问题;
但如果写成 setSeconds(seconds + 1) 且依赖为空 → 秒数永远是 1
③ 快捷键(filterTags / messages)
写进依赖 → 每次消息来都重新绑定键盘监听(高频重绑)
不写依赖 → Esc 永远按首次的 filterTags 判断
→ useEvent 让「三个 Effect 的依赖」都退化成「业务上真正的重建条件」
(roomId、delay、key),而不是「回调里用到的每一个值」。
五、与 useCallback + 依赖的关系
// ❌ 用 useCallback + 全依赖:引用会变 → 副作用重建
const onMessage = useCallback((m) => {
if (filterTags.includes(m.tag)) append(m);
}, [filterTags]); // filterTags 变 → onMessage 变 → 订阅重建
useEffect(() => {
socket.onmessage = onMessage;
}, [onMessage]); // ⚠️ 随 filterTags 重建
// ✅ 用 Effect Event:引用不变 → 副作用不重建
const onMessage = useEvent((m) => {
if (filterTags.includes(m.tag)) append(m); // 永远读最新 filterTags
});
useEffect(() => {
socket.onmessage = onMessage;
}, [onMessage]); // ⭐ 引用稳定 → 只随「真正的依赖」重建
| 维度 | useCallback(fn, [deps]) |
useEvent(fn) |
|---|---|---|
| 引用稳定性 | 依赖变化时会变 | 永远不变 |
| 闭包内容 | 创建它那次渲染的快照 | 调用时的最新值 |
| 是否需要写依赖 | ✅ 必须写全 | ❌ 永远不写 |
| 调用位置限制 | 无 | 只能从 Effect 里调用 |
| 能否传给子组件 | ✅(这是它的常见用途) | ❌(语义不允许) |
| 典型用途 | 稳定 props 引用(配合 memo) | Effect 内部读最新值 |
⭐ 一句话区分:
useCallback 解决「**给子组件的 props 引用稳定**」
useEvent 解决「**给 Effect 的回调读到最新值**」
两者目的不同,不能互相替代。
六、为什么「不能传给子组件」
// ❌ 反例:把 Effect Event 当回调 props 传下去
function Parent() {
const onSelect = useEvent((id: string) => {
setSelected(id);
log('selected', selectedCount); // ⚠️ 读的是「最新值」
});
return ;
}
function Child({ onSelect }: { onSelect: (id: string) => void }) {
// ⚠️ 这里在「渲染」里使用了它(比如算出一个依赖它的值)
const handle = useMemo(() => () => onSelect('x'), [onSelect]);
// ...
}
⭐⭐ 为什么不允许?
因为 Effect Event 的语义是「**读取最新值的私有通道**」,
它「脱离了 React 的响应式追踪」。
如果它被传给子组件并在「渲染期」被使用,就会出现:
一次渲染的输出,取决于「调用它那一刻的 ref 内容」,
而不是「这次渲染的 props/state」。
→ 破坏「UI = f(props, state)」这个不变量
→ 出现「同样的 props 渲染出不同结果」的现象(在并发渲染下更危险)
⭐ 所以规则是:
Effect Event **属于「副作用的世界」,不属于「渲染的世界」**。
渲染世界的东西用 useCallback(并可传给子组件);
副作用世界的东西用 useEvent(且只留在 Effect 里)。
七、三条硬性约束与失败路径
// ① ❌ 在渲染期间调用
function Bad1({ roomId }: { roomId: string }) {
const onVisit = useEvent(() => logVisit(roomId));
onVisit(); // ❌ 渲染期间调用 Effect Event
return ;
// ⚠️ 语义错误:这等于「在渲染里做副作用」,而且它不受依赖控制
}
// ② ❌ 在事件处理里调用
function Bad2() {
const onSave = useEvent(() => save(latestDraft));
return ;
// ❌ Effect Event 不是给事件用的——
// 事件处理里应该直接用当前的 state/props(它们本来就是最新的)
}
// ③ ❌ 写进依赖数组
useEffect(() => {
const id = setInterval(onTick, 1000);
return () => clearInterval(id);
}, [onTick]); // ⚠️ 虽然「因为引用稳定所以无害」,但语义上是错的
// ✅ 官方要求:Effect Event 永远从依赖里省略
useEffect(() => {
const id = setInterval(onTick, 1000);
return () => clearInterval(id);
}, []);
⭐ 三条约束的记忆方法(三个世界):
渲染世界 → props / state / useMemo / useCallback / 子组件
事件世界 → onClick / onChange 里的处理函数(读当次渲染的值即可)
副作用世界 → Effect 内部 / useEffectEvent(读最新值)
Effect Event 是「副作用世界」的居民,
它【不能进入渲染世界】(不能传给子组件、不能在渲染中调用),
也【不需要进入事件世界】(事件里本来就是最新值)。
八、ESLint 与依赖数组的关系
// 有了 useEffectEvent 后,exhaustive-deps 的行为变化
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
showNotification('已连接', theme); // ⚠️ theme 未在依赖里 → lint 警告
});
return () => connection.disconnect();
}, [roomId]);
// ✅ 用 Effect Event 后,lint 不再要求把 theme 写进依赖
const onConnected = useEffectEvent(() => {
showNotification('已连接', theme); // ⭐ lint 认识它:内部读的值不算 Effect 的依赖
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected);
return () => connection.disconnect();
}, [roomId]); // ✅ 无警告
⭐ 这是它相比「自研 useEvent」的一个实际优势:
自研版本(用 useRef + useInsertionEffect)虽然「运行时行为一致」,
但 lint 不认识它——如果你在 Effect 里直接写内联函数读 theme,
警告依然存在(你必须用「把函数定义在 Effect 内部」或「disable 注释」来消警告)。
⚠️ 但注意:自研 useEvent 版本可以这样写(把逻辑放进 useEvent,Effect 里只用稳定引用),
此时 lint 只看到「onConnected 是稳定引用」→ 也不再警告。
→ 所以「运行时等价」,差别在「语义表达力」与「官方支持」。
九、迁移路径与现状
⭐ 现状(截至 React 19):
· `useEffectEvent` 仍处于【实验阶段】(experimental / canary 通道)
· 它的前身叫 `useEvent`(在 2022 年被改名,因为社区对语义有争议)
· 稳定版本发布前,生产环境应使用「useRef 保存最新回调」的模式
⭐ 三种可用方案的取舍:
| 方案 | 运行时行为 | lint 友好 | 生产可用 | 语义表达 |
| --- | --- | --- | --- | --- |
| 自研 useEvent(useRef + useInsertionEffect) | ✅ 与官方一致 | ⚠️ 部分 | ✅ | 中 |
| Effect Event(实验) | ✅ 官方实现 | ✅ | ❌ 需 canary | ✅ 最强 |
| 直接把函数定义在 Effect 内部 | ✅ | ✅ | ✅ | ⚠️ 只在「不需要复用」时可用 |
| eslint-disable + 空依赖 | ⚠️ 易错 | ❌ | ✅ | 最差 |
⭐ 迁移建议:
① 现在:统一用自研 useEvent(封装在一个文件里,将来只改一处)
② 等 useEffectEvent 稳定:把实现切换成官方版本(对外 API 不变)
③ 同时清理历史遗留的 `eslint-disable-next-line react-hooks/exhaustive-deps`
// ✅ 推荐的落地结构:一个「Effect Event 适配层」
// hooks/use-event.ts
import { useCallback, useRef, useInsertionEffect } from 'react';
/**
* Effect Event 的适配层。
* 等 React 的 useEffectEvent 稳定后,只需把这里换成:
* import { useEffectEvent } from 'react';
* export const useEvent = useEffectEvent;
* 业务代码不需要改动。
*/
export function useEvent(handler: (...args: Args) => R): (...args: Args) => R {
const handlerRef = useRef(handler);
useInsertionEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args: Args) => handlerRef.current(...args), []);
}
十、什么时候不该用它
⭐ 三条「先别用」的判断:
① 如果「重建副作用」在业务上就是对的 → 直接写依赖
例:请求要随 keyword 变化 → useEffect(() => { fetch(keyword) }, [keyword])
(这里 keyword 变化时「重新请求」正是你想要的行为)
② 如果「不需要最新值」→ 不需要它
例:只在挂载时上报一次 → useEffect(() => { track('mount') }, [])
(回调里不读任何会变的值,自然没有 stale 问题)
③ 如果能「把函数定义在 Effect 内部」→ 优先这么做(更简单、没有额外抽象)
useEffect(() => {
function handle() { console.log(roomId); } // ⭐ 依赖变成 roomId(清晰)
window.addEventListener('keydown', handle);
return () => window.removeEventListener('keydown', handle);
}, [roomId]);
⭐ 它真正的适用场景只有一类:
「回调需要读到最新值,而这个值【不应该】成为重建副作用的理由」。
—— 典型即「订阅/连接/定时器的回调里读 UI 状态」。
十一、本篇特有的坑
// ① 用 useEffect(而不是 useInsertionEffect)更新 ref → layout effect 里读到旧值
function useEventWrong(handler: (...a: A) => R) {
const ref = useRef(handler);
useEffect(() => { ref.current = handler; }); // ⚠️ 太晚
return useCallback((...a: A) => ref.current(...a), []);
}
// 现象:在 useLayoutEffect 里调用它时,拿到的是「上一轮渲染」的 handler
// ✅ 改成 useInsertionEffect
// ② 把 Effect Event 写进依赖数组(语义错误,虽无害)
}, [onTick]); // ⚠️ 官方要求省略
// ③ 在渲染期间调用 Effect Event
onVisit(); // ❌
// ④ 把 Effect Event 传给子组件
// ❌
// ⑤ 用 Effect Event「读最新值」去做「本该重新请求」的事
const onSubmit = useEffectEvent(() => fetch(`/api?q=${keyword}`)); // ⚠️ 用错场景
useEffect(() => { onSubmit(); }, []); // keyword 变了却不请求
// ✅ 应该把 keyword 写进依赖
// ⑥ 在 Effect Event 里「做同步的状态更新」导致无限循环
const onTick = useEffectEvent(() => setCount(count + 1)); // ⚠️ 每次 tick 都基于最新 count
// ⚠️ 虽然不会 stale,但如果是「在 Effect 里直接调用它」就会形成循环
// ✅ 用函数式更新 setCount(c => c + 1)
// ⑦ 自研版本用「useState 存 handler」→ 每次渲染都多一次 re-render
const [handler, setHandler] = useState(() => fn); // ❌ 用 ref
useEffect(() => { setHandler(() => fn); }); // 会触发额外渲染
// ⑧ 自研版本「每次渲染都重建返回函数」(忘了 useCallback)
return (...args) => handlerRef.current(...args); // ❌ 引用不稳定 → 副作用仍会重建
// ⑨ 依赖数组里同时写 onTick 和「它读的值」→ 等于没用 Effect Event
}, [onTick, filterTags]); // ⚠️ 白做
// ⑩ 用 Effect Event「绕过」lint 警告,但实际上是自己写错了依赖
// 例:请求类 Effect 里用 Effect Event 读参数 → 掩盖了「应该重新请求」的语义错误
// ⑪ 在「非 Effect 的自定义 Hook」里返回 Effect Event 给调用方使用
// ⚠️ 如果不加限制,调用方可能在渲染里用它(违反语义)
// ⑫ 认为「Effect Event 能解决所有 stale closure」
// ⚠️ 它只解决「Effect 内部」的 stale;
// 事件处理里的 stale 要靠「函数式更新」或「依赖修正」
十二、面试延伸
- 「
useEffectEvent解决什么问题?」
它解决「Effect 里需要读最新值,但不想因此重建副作用」这个矛盾。典型场景是「订阅 / 连接 / 定时器的回调里读 UI 状态」:把theme写进依赖会导致「主题一变就重连」(副作用被无谓重建),不写依赖又会「读到旧值」(闭包陷阱)。useEffectEvent返回的函数「引用永远稳定 + 内部永远读到最新值 + 只能从 Effect 里调用」,从而让 Effect 的依赖数组退化成「业务上真正的重建条件」。 -
「它和
useCallback有什么区别?」
两者解决不同问题:①useCallback用于「给子组件的 props 提供稳定引用」(配合React.memo)——它的闭包内容依然是「创建它那次渲染的快照」,所以必须写全依赖,依赖一变引用就变;②useEffectEvent用于「Effect 内部读最新值」——它的引用永远不变,闭包内容永远是调用时的最新值,且永远不写进依赖。此外useEffectEvent只能从 Effect 里调用(不能传子组件、不能在渲染中用)。 -
「为什么要用
useInsertionEffect更新 ref,而不是useEffect?」
因为 Effect 的执行顺序是useInsertionEffect(DOM 变更前)→useLayoutEffect(绘制前)→useEffect(绘制后)。如果 ref 用useEffect更新,那么在useLayoutEffect里调用这个 Effect Event 时,ref 里还是「上一轮渲染的 handler」(读到旧值);用useInsertionEffect更新则能保证「在任何 Effect 里调用都能拿到本轮最新值」。这也是 React 官方useEffectEvent的内部实现方式。 -
「为什么它不能传给子组件?」
因为它的语义是「读取最新值的私有通道」,它脱离了 React 的响应式追踪。如果传给子组件并在「渲染期」使用,一次渲染的输出就会取决于「调用它那一刻的 ref 内容」,而不是「这次渲染的 props/state」——破坏了「UI = f(props, state)」这个不变量(并发渲染下更危险:可能渲染出与 props 不一致的结果)。所以规则是「Effect Event 属于副作用的世界,不属于渲染的世界」。 -
「它现在能用在生产环境吗?」
截至 React 19,useEffectEvent仍处于实验阶段(canary/experimental),生产环境不应用它。替代方案是自研useEvent:用useRef保存最新 handler(在useInsertionEffect里更新),再用useCallback(() => ref.current(...args), [])返回稳定引用。它的运行时行为与官方版本一致(连「用 insertion effect 更新 ref」这个细节都一致),差别只在「lint 是否认识」和「语义表达力」。 -
「加了它还需要
exhaustive-deps吗?」
需要,但警告会变少:有了 Effect Event 后,那些「回调里读的、但不应作为依赖的值」可以放进 Effect Event 内部,lint 不会再要求把它们写进依赖数组。注意两点:① 自研的useEventlint 不认识(它只知道useEffectEvent),但你只要「在 Effect 里使用稳定引用」就不会触发警告;② 不要用它来掩盖「应该重新请求」的语义错误——例如「请求类 Effect 用 Effect Event 读keyword」,那会让「参数变化时不重新请求」,是真正的 bug。 -
「什么时候不该用它?」
三种情况:① 重建副作用在业务上就是对的——如「keyword 变化要重新请求」,直接写依赖即可;② 回调里不需要最新值——如「挂载时上报一次」,没有 stale 问题;③ 能把函数定义在 Effect 内部——那就直接定义(依赖会自然变成函数用到的值,更简单、没有额外抽象)。它真正的适用场景只有一类:「回调需要读最新值,而这个值不应该成为重建副作用的理由」。 -
「它和
useRef保存最新值是什么关系?」
本质是同一件事,但useEffectEvent更「语义化」:useRef方案(useEffect(() => { ref.current = fn }))是「手动的、lint 不认识的、容易被用错的」;useEffectEvent是「官方承认的、lint 认识的、有明确调用边界的」(只能 Effect 里用、不写依赖、不传子组件)。关系是「同一种机制的『原始版』与『规范版』」——useEffectEvent内部就是用 insertion effect 更新 ref 实现的。 -
「如果我有十个 Effect Event,会有性能问题吗?」
没有明显开销:每个 Effect Event 只是「一个 ref + 一个稳定的 useCallback」,渲染期的工作量是「一次 ref 赋值」(在 insertion effect 里)。真正的成本是「抽象层级」——如果在一个组件里放十个 Effect Event,说明这个组件的副作用很多,应该考虑「拆分成多个自定义 Hook」(每个 Hook 封装一类副作用 + 它的 Effect Event)。所以「Effect Event 的数量」更多是「设计信号」,而不是「性能问题」。 -
「
useEffect里的闭包问题,有了它就不用管了吗?」
不是。Effect Event 只解决「Effect 内部回调读最新值」这一类;闭包陷阱还有另外两个来源:① 事件处理函数里的 stale——但事件处理函数的生命周期就是「本次渲染」,通常不需要处理(除非它被传给了setTimeout/ 第三方库缓存);②useCallback/useMemo里的 stale——那要靠「写全依赖」解决。所以「Effect Event 是补充手段,不是替代品」;主路径依然是「写全依赖」。
一句话速记
useEffectEvent解决「Effect 里要读最新值但不想重建副作用」——它返回一个「引用永久稳定 + 内部永远是最新值 + 只能从 Effect 里调用」的函数;三条铁律:「只从 Effect 里调用、永不写进依赖、永不传给子组件」(因为它属于「副作用世界」,进入渲染世界会破坏UI = f(props, state));它与useCallback目的不同(后者是「给子组件的稳定 props」,必须写全依赖);可用自研useEvent等价替代——关键是「用useInsertionEffect(不是useEffect)更新 ref」,否则在useLayoutEffect里会读到上一轮的 handler;React 19 中仍是实验特性(建议做一层「适配层」,等官方稳定后只改一处);别用它掩盖「本该重新请求」的语义错误——能写依赖就写依赖,能把函数放进 Effect 就放进去。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!