useEffectEvent 是一个 React Hook,它让你将事件与副作用分开。
const onEvent = useEffectEvent(callback)参考
🌐 Reference
useEffectEvent(callback)
在组件的顶层调用 useEffectEvent 来创建一个 Effect 事件。
🌐 Call useEffectEvent at the top level of your component to create an Effect Event.
import { useEffectEvent, useEffect } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
}效果事件是你效果逻辑的一部分,但它们的行为更像是事件处理程序。它们总是“看到”来自渲染的最新值(如 props 和 state),而不会重新同步你的效果,因此它们被排除在效果依赖之外。参见 将事件与效果分离 以了解更多信息。
🌐 Effect Events are a part of your Effect logic, but they behave more like an event handler. They always “see” the latest values from render (like props and state) without re-synchronizing your Effect, so they’re excluded from Effect dependencies. See Separating Events from Effects to learn more.
参数
🌐 Parameters
callback:一个包含你的效果事件逻辑的函数。该函数可以接受任意数量的参数并返回任意值。当你调用返回的效果事件函数时,callback总是访问调用时渲染中最新提交的值。
返回
🌐 Returns
useEffectEvent 返回一个效果事件函数,其类型签名与你的 callback 相同。
你可以在 useEffect、useLayoutEffect、useInsertionEffect 中调用此函数,或者在同一组件中的其他效果事件内调用它。
🌐 You can call this function inside useEffect, useLayoutEffect, useInsertionEffect, or from within other Effect Events in the same component.
注意事项
🌐 Caveats
useEffectEvent是一个 Hook,因此你只能在组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,可以提取一个新组件,并将 Effect 事件移到其中。- 效果事件只能从效果或其他效果事件内部调用。不要在渲染期间调用它们,也不要将它们传递给其他组件或钩子。
eslint-plugin-react-hooks检查器强制执行此限制。 - 不要使用
useEffectEvent来避免在 Effect 的依赖数组中指定依赖。这会隐藏错误并使你的代码更难理解。只有在逻辑确实是由 Effects 触发的事件时才使用它。 - Effect Event 函数没有稳定的身份。它们的身份在每次渲染时都会故意改变。
深入研究
🌐 Why are Effect Events not stable?
与来自 useState 或 refs 的 set 函数不同,Effect 事件函数没有稳定的身份。它们的身份在每次渲染时都会有意地改变:
🌐 Unlike set functions from useState or refs, Effect Event functions do not have a stable identity. Their identity intentionally changes on every render:
// 🔴 Wrong: including Effect Event in dependencies
useEffect(() => {
onSomething();
}, [onSomething]); // ESLint will warn about this这是一个故意的设计选择。Effect 事件的设计目的是仅在同一组件中的 Effect 内部调用。由于你只能在本地调用它们,不能将它们传递给其他组件,也不能将它们包含在依赖数组中,因此稳定的标识没有任何作用,实际上还会掩盖错误。
🌐 This is a deliberate design choice. Effect Events are meant to be called only from within Effects in the same component. Since you can only call them locally and cannot pass them to other components or include them in dependency arrays, a stable identity would serve no purpose, and would actually mask bugs.
非稳定的身份充当运行时断言:如果你的代码错误地依赖于函数身份,你会看到 Effect 在每次渲染时重新运行,从而使错误变得明显。
🌐 The non-stable identity acts as a runtime assertion: if your code incorrectly depends on the function identity, you’ll see the Effect re-running on every render, making the bug obvious.
这个设计强化了这样的概念:效果事件在概念上属于特定的效果,而不是用于选择退出响应性的通用 API。
🌐 This design reinforces that Effect Events conceptually belong to a particular effect, and are not a general purpose API to opt-out of reactivity.
用法
🌐 Usage
在效果中使用事件
🌐 Using an event in an Effect
在组件的顶层调用 useEffectEvent 来创建一个效果事件:
🌐 Call useEffectEvent at the top level of your component to create an Effect Event:
const onConnected = useEffectEvent(() => {
if (!muted) {
showNotification('Connected!');
}
});useEffectEvent 接受一个 event callback 并返回一个 效果事件。效果事件是在效果内部可以调用的函数,而无需重新连接效果:
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected);
connection.connect();
return () => {
connection.disconnect();
}
}, [roomId]);由于 onConnected 是一个 效果事件,muted 和 onConnect 不在效果依赖中。
使用带有最新值的计时器
🌐 Using a timer with latest values
当你在 Effect 中使用 setInterval 或 setTimeout 时,你通常希望在这些值发生变化时,从渲染中读取最新的值,而不重新启动计时器。
🌐 When you use setInterval or setTimeout in an Effect, you often want to read the latest values from render without restarting the timer whenever those values change.
这个计数器每秒将 count 增加当前的 increment 值。onTick 效果事件读取最新的 count 和 increment,而不会导致间隔重新开始:
🌐 This counter increments count by the current increment value every second. The onTick Effect Event reads the latest count and increment without causing the interval to restart:
import { useState, useEffect, useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); const onTick = useEffectEvent(() => { setCount(count + increment); }); useEffect(() => { const id = setInterval(() => { onTick(); }, 1000); return () => { clearInterval(id); }; }, []); return ( <> <h1> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
尝试在计时器运行时更改增量值。计数器会立即使用新的增量值,但计时器会保持平稳计时而不会重新启动。
🌐 Try changing the increment value while the timer is running. The counter immediately uses the new increment value, but the timer keeps ticking smoothly without restarting.
使用带有最新值的事件监听器
🌐 Using an event listener with latest values
当你在 Effect 中设置事件监听器时,你通常需要在回调中读取渲染的最新值。没有 useEffectEvent,你需要将这些值包括在依赖中,这会导致监听器在每次变化时被移除并重新添加。
🌐 When you set up an event listener in an Effect, you often need to read the latest values from render in the callback. Without useEffectEvent, you would need to include the values in your dependencies, causing the listener to be removed and re-added on every change.
此示例显示了一个跟随光标的点,但仅当勾选“可以移动”时才会跟随。onMove 效果事件始终读取最新的 canMove 值,而无需重新运行效果:
🌐 This example shows a dot that follows the cursor, but only when “Can move” is checked. The onMove Effect Event always reads the latest canMove value without re-running the Effect:
import { useState, useEffect, useEffectEvent } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); const onMove = useEffectEvent(e => { if (canMove) { setPosition({ x: e.clientX, y: e.clientY }); } }); useEffect(() => { window.addEventListener('pointermove', onMove); return () => window.removeEventListener('pointermove', onMove); }, []); return ( <> <label> <input type="checkbox" checked={canMove} onChange={e => setCanMove(e.target.checked)} /> The dot is allowed to move </label> <hr /> <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity: 0.6, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> </> ); }
切换复选框并移动光标。圆点会立即响应复选框的状态,但事件监听器只在组件挂载时设置一次。
🌐 Toggle the checkbox and move your cursor. The dot responds immediately to the checkbox state, but the event listener is only set up once when the component mounts.
避免重新连接到外部系统
🌐 Avoid reconnecting to external systems
useEffectEvent 的一个常见用例是当你想要对一个 Effect 做出某种响应时,但这个“某种响应”依赖于一个你不想做出反应的值。
🌐 A common use case for useEffectEvent is when you want to do something in response to an Effect, but that “something” depends on a value you don’t want to react to.
在此示例中,聊天组件会连接到一个房间,并在连接时显示通知。用户可以通过复选框静音通知。然而,你不希望每次用户更改设置时都重新连接到聊天房间:
🌐 In this example, a chat component connects to a room and shows a notification when connected. The user can mute notifications with a checkbox. However, you don’t want to reconnect to the chat room every time the user changes the settings:
import { useState, useEffect, useEffectEvent } from 'react'; import { createConnection } from './chat.js'; import { showNotification } from './notifications.js'; function ChatRoom({ roomId, muted }) { const onConnected = useEffectEvent((roomId) => { console.log('✅ Connected to ' + roomId + ' (muted: ' + muted + ')'); if (!muted) { showNotification('Connected to ' + roomId); } }); useEffect(() => { const connection = createConnection(roomId); console.log('⏳ Connecting to ' + roomId + '...'); connection.on('connected', () => { onConnected(roomId); }); connection.connect(); return () => { console.log('❌ Disconnected from ' + roomId); connection.disconnect(); } }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); const [muted, setMuted] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={muted} onChange={e => setMuted(e.target.checked)} /> Mute notifications </label> <hr /> <ChatRoom roomId={roomId} muted={muted} /> </> ); }
尝试切换房间。聊天会重新连接并显示通知。现在将通知静音。由于 muted 是在 Effect 事件中读取而不是在 Effect 中读取,聊天会保持连接。
🌐 Try switching rooms. The chat reconnects and shows a notification. Now mute the notifications. Since muted is read inside the Effect Event rather than the Effect, the chat stays connected.
在自定义 Hook 中使用 Effect 事件
🌐 Using Effect Events in custom Hooks
你可以在你自己的自定义 Hook 中使用 useEffectEvent。这让你可以创建可重用的 Hook,在封装 Effect 的同时保持某些值为非响应式:
🌐 You can use useEffectEvent inside your own custom Hooks. This lets you create reusable Hooks that encapsulate Effects while keeping some values non-reactive:
import { useState, useEffect, useEffectEvent } from 'react'; function useInterval(callback, delay) { const onTick = useEffectEvent(callback); useEffect(() => { if (delay === null) { return; } const id = setInterval(() => { onTick(); }, delay); return () => clearInterval(id); }, [delay]); } function Counter({ incrementBy }) { const [count, setCount] = useState(0); useInterval(() => { setCount(c => c + incrementBy); }, 1000); return ( <div> <h2>Count: {count}</h2> <p>Incrementing by {incrementBy} every second</p> </div> ); } export default function App() { const [incrementBy, setIncrementBy] = useState(1); return ( <> <label> Increment by:{' '} <select value={incrementBy} onChange={(e) => setIncrementBy(Number(e.target.value))} > <option value={1}>1</option> <option value={5}>5</option> <option value={10}>10</option> </select> </label> <hr /> <Counter incrementBy={incrementBy} /> </> ); }
在这个例子中,useInterval 是一个设置间隔的自定义 Hook。传递给它的 callback 被封装在一个 Effect 事件中,因此即使每次渲染时传入新的 callback,间隔也不会重置。
🌐 In this example, useInterval is a custom Hook that sets up an interval. The callback passed to it is wrapped in an Effect Event, so the interval does not reset even if a new callback is passed in every render.
故障排除
🌐 Troubleshooting
我收到一个错误:“在渲染期间不能调用用 useEffectEvent 封装的函数”
🌐 I’m getting an error: “A function wrapped in useEffectEvent can’t be called during rendering”
这个错误意味着你在组件的渲染阶段调用了 Effect Event 函数。Effect Events 只能在 Effects 或其他 Effect Events 内部调用。
🌐 This error means you’re calling an Effect Event function during the render phase of your component. Effect Events can only be called from inside Effects or other Effect Events.
function MyComponent({ data }) {
const onLog = useEffectEvent(() => {
console.log(data);
});
// 🔴 Wrong: calling during render
onLog();
// ✅ Correct: call from an Effect
useEffect(() => {
onLog();
}, []);
return <div>{data}</div>;
}如果你需要在渲染期间运行逻辑,不要将其封装在 useEffectEvent 中。直接调用逻辑或将其移到 Effect 中。
🌐 If you need to run logic during render, don’t wrap it in useEffectEvent. Call the logic directly or move it into an Effect.
我遇到了一个 lint 错误:“从 useEffectEvent 返回的函数不能包含在依赖数组中”
🌐 I’m getting a lint error: “Functions returned from useEffectEvent must not be included in the dependency array”
如果你看到类似“从 useEffectEvent 返回的函数不能包含在依赖数组中”的警告,请从你的依赖中移除 Effect 事件:
🌐 If you see a warning like “Functions returned from useEffectEvent must not be included in the dependency array”, remove the Effect Event from your dependencies:
const onSomething = useEffectEvent(() => {
// ...
});
// 🔴 Wrong: Effect Event in dependencies
useEffect(() => {
onSomething();
}, [onSomething]);
// ✅ Correct: no Effect Event in dependencies
useEffect(() => {
onSomething();
}, []);效果事件被设计为可以从效果中调用,而无需列为依赖。代码检查器之所以执行此规则,是因为函数身份是故意不稳定的。将其包括在内会导致你的效果在每次渲染时重新运行。
🌐 Effect Events are designed to be called from Effects without being listed as dependencies. The linter enforces this because the function identity is intentionally not stable. Including it would cause your Effect to re-run on every render.
我收到一个 lint 错误:“… 是一个使用 useEffectEvent 创建的函数,只能从 Effects 中调用”
🌐 I’m getting a lint error: ”… is a function created with useEffectEvent, and can only be called from Effects”
如果你看到类似“…是一个由 React Hook useEffectEvent 创建的函数,只能从 Effects 和 Effect Events 中调用”的警告,说明你在错误的地方调用了该函数:
🌐 If you see a warning like ”… is a function created with React Hook useEffectEvent, and can only be called from Effects and Effect Events”, you’re calling the function from the wrong place:
const onSomething = useEffectEvent(() => {
console.log(value);
});
// 🔴 Wrong: calling from event handler
function handleClick() {
onSomething();
}
// 🔴 Wrong: passing to child component
return <Child onSomething={onSomething} />;
// ✅ Correct: calling from Effect
useEffect(() => {
onSomething();
}, []);效果事件是专门设计用于在其定义的组件本地使用的效果。如果你需要用于事件处理程序的回调或传递给子组件,请改用常规函数或 useCallback。
🌐 Effect Events are specifically designed to be used in Effects local to the component they’re defined in. If you need a callback for event handlers or to pass to children, use a regular function or useCallback instead.