什么是 useEffectEvent ?
useEffectEvent 是一个 React Hook,它允许你将非响应式逻辑从 Effects 中提取出来。它解决了常见的问题:当你需要在 Effect 内部读取 props 或 state 的最新值时,却不希望这些值改变时导致该 Effect 重新运行。
核心概念:
useEffectEvent创建一个稳定的函数引用,该引用始终读取最新值,但当这些值发生变化时不会触发 Effect 的重新执行。
解决的问题
假设有一个聊天应用,你希望在房间变更时记录消息,但需要在日志中包含当前主题。传统做法是将主题添加到依赖数组中,这会导致每次主题变更时效果都会重新运行——尽管你只希望对房间变更做出响应。
❌ 传统问题:
useEffect(() => {
logVisit(roomId, theme); // Re-runs when theme changes
}, [roomId, theme]); // Had to include theme!
✅ 使用 useEffectEvent:
const onVisit = useEffectEvent((roomId) => {
logVisit(roomId, theme); // Reads latest theme
});
useEffect(() => {
onVisit(roomId); // Only re-runs when roomId changes
}, [roomId]);
Do's and Don'ts
✓ 建议
- 用于读取最新 props/state 而不将其添加到依赖项中
- 直接在 Effects 内部调用
- 用于需要访问 Effect 上下文的事件处理程序
- 用于分离响应式与非响应式逻辑
- 在 Effect 内部同步调用
✗ 禁止事项
- 禁止在常规事件处理程序中调用
- 禁止在渲染过程中调用
- 禁止将其作为组件 props 传递
- 禁止异步调用或延迟调用
- 禁止将其作为正确备忘录机制的替代方案
详细示例
✅ 操作:提取非反应性逻辑
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // Only roomId is reactive
}
此效果仅在roomId变更时重新连接,但onConnected事件始终采用最新theme。
❌ 禁止:从事件处理程序中调用
function Component() {
const onClick = useEffectEvent(() => {
// ❌ Wrong! Don't use in event handlers
doSomething();
});
return <button onClick={onClick}>Click</button>;
}
请改用常规函数或 useCallback 作为事件处理程序。
✅ 操作指南:在效果中读取最新道具
function Timer({ interval, onTick }) {
const onTickEvent = useEffectEvent(() => {
onTick(); // Always calls latest onTick
});
useEffect(() => {
const id = setInterval(onTickEvent, interval);
return () => clearInterval(id);
}, [interval]); // Only interval is reactive
}
❌ 不要:异步调用
function Component() {
const onData = useEffectEvent((data) => {
processData(data);
});
useEffect(() => {
fetchData().then(data => {
onData(data); // ❌ Risky! Called asynchronously
});
}, []);
}
该函数可能在组件卸载后或值发生变化后被调用。
✅ 操作:与清理逻辑结合使用
function Analytics({ userId, page }) {
const logPageView = useEffectEvent(() => {
analytics.track('page_view', { userId, page });
});
useEffect(() => {
logPageView();
return () => {
// Cleanup can also use Effect Events
const logPageExit = useEffectEvent(() => {
analytics.track('page_exit', { userId, page });
});
logPageExit();
};
}, []); // Empty deps - runs once per mount
}
❌ 禁止:将依赖项传递给其他钩子
function Component() {
const onEvent = useEffectEvent(() => {
doSomething();
});
// ❌ Don't do this
const memoized = useMemo(() => {
return onEvent();
}, [onEvent]);
}
常见使用场景
1. 日志记录与分析 当您需要记录包含最新用户偏好或设置的事件时,无需重新订阅。
const logEvent = useEffectEvent((eventName) => {
analytics.log(eventName, { theme, locale, userId });
});
useEffect(() => {
logEvent('page_visit');
}, [pathname]); // Only react to pathname changes
2. 带最新状态的回调 在向第三方库传递回调函数时,不应导致重新订阅。
const onMessage = useEffectEvent((msg) => {
showToast(msg, { variant: userPreference });
});
useEffect(() => {
const unsubscribe = messageService.subscribe(onMessage);
return unsubscribe;
}, []); // Subscribe once, callback uses latest userPreference
3. 采用最新值实现去抖动 在实现防抖动时始终使用最新的回调逻辑。
const onSearch = useEffectEvent(() => {
performSearch(query, filters, sortBy);
});
useEffect(() => {
const timeoutId = setTimeout(onSearch, 500);
return () => clearTimeout(timeoutId);
}, [query]); // Debounce query, but use latest filters/sortBy
最佳实践
- 仅在确认确实需要读取非响应式值时使用
useEffectEvent - 请考虑您的逻辑是否真正属于 Effect,还是可置于事件处理程序中
- 确保 Event 函数专注于单一目的
- 记录使用原因以便未来维护者参考
- 请在稳定版本发布后再用于生产环境应用
迁移路径
如果你当前正在使用useCallback处理不断变化的依赖项,或通过eslint-disable注释来抑制代码检查器,useEffectEvent可能是你需要的解决方案。不过,请先考虑是否更适合重构组件逻辑。
结论
useEffectEvent 是一款强大的工具,可解决在 Effects 中读取最新值时避免不必要重新执行的难题。遵循这些注意事项,当该功能稳定后,您就能有效地使用它。请记住,它适用于特定场景——即需要在 Effects 中将响应式逻辑与非响应式逻辑分离的情况。