Read the latest callbacks in Effects without resubscribing
When an Effect should re-run only for some values but also needs the latest version of others, such as a callback prop, read those others through useEffectEvent, or through a ref in React versions without it.
Keep only the values that should restart the Effect in its dependency array.
Implementation
- In React 19.2 or newer, wrap the non-reactive logic in
useEffectEventand call the returned function from inside the Effect or the subscriptions it creates. - Do not put an Effect Event in a dependency array; its identity changes on every render by design.
- Call Effect Events only from Effects or other Effect Events in the same component. Do not call them during render or pass them to other components or Hooks.
- In older React versions, store the latest callback in a ref updated by an Effect, and call
ref.currentfrom the subscription. - Keep values that should restart the Effect, such as a room id or event type, as ordinary dependencies.
Rationale
An Effect re-runs whenever a dependency changes. Callback props are usually new functions on every render, so listing one restarts the subscription on every render. Leaving it out without an Effect Event or ref makes the Effect call the version from its first render, with stale props and state.
Examples
Application: A subscription that calls a callback prop
Incorrect (counterexample):
function useWindowEvent(type: string, handler: (event: Event) => void) {
useEffect(() => {
window.addEventListener(type, handler);
return () => window.removeEventListener(type, handler);
}, [type, handler]);
}
Callers usually pass an inline function, so the listener is removed and added again on every render.
Correct (React 19.2 or newer):
function useWindowEvent(type: string, handler: (event: Event) => void) {
const onEvent = useEffectEvent(handler);
useEffect(() => {
const listener = (event: Event) => onEvent(event);
window.addEventListener(type, listener);
return () => window.removeEventListener(type, listener);
}, [type]);
}
Correct (earlier React versions):
function useWindowEvent(type: string, handler: (event: Event) => void) {
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const listener = (event: Event) => handlerRef.current(event);
window.addEventListener(type, listener);
return () => window.removeEventListener(type, listener);
}, [type]);
}
Application: An Effect Event in dependencies
Incorrect (counterexample):
const onConnected = useEffectEvent(onConnectedProp);
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId, onConnected]);
The Effect Event's identity changes every render, so the connection restarts every render, and the Hooks lint rule reports it.
Correct:
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]);
Validation
Check that the React Hooks lint rules pass without suppressions for these Effects. Re-render the component with a new callback prop and check that the subscription is not recreated.
Listing a callback as a dependency is not a violation when the Effect should restart whenever that callback changes.