In React 19.2, an effect reconnects a chat socket whenever `roomId` changes, but its body also calls the latest `onConnected` prop and reads the current `theme`; listing those in the dependency array makes the socket reconnect every time they change. What does useEffectEvent do here, and what rules govern it?
answer
- split reactive from non-reactive logic
- always reads the latest props and state
- never a dependency, never a prop
- the room is reactive, the theme is not
- not an escape hatch for the deps rule
basics
~20 suseEffectEvent extracts the non-reactive part of an effect into a function that always sees the latest props and state but is not itself a dependency. The effect calls it, so the socket reconnects only when roomId changes.
solid answer
~50 sIt splits the effect into reactive and non-reactive logic. Connecting to a room is reactive: `roomId` is genuinely part of what should be synchronized, so a change must tear down and reconnect. Notifying `onConnected` and reading `theme` are not — they are things you want the newest value of at the moment the event happens, not reasons to re-establish the connection. `useEffectEvent(fn)` returns a stable function whose body always reads the latest render's props and state, and the lint rule excludes it from the dependency array, so the effect can keep `[roomId]` and stay honest. The rules are narrow: declare it in the same component as the effect that uses it, call it only from effects of that component, never call it during render, never list it as a dependency, and never pass it to a child or into another hook.
code
javascript · 17 linesimport { useEffect, useEffectEvent } from 'react';
import { createConnection } from './chat';
export function ChatRoom({ roomId, theme, onConnected }) {
const onConnectedEvent = useEffectEvent(() => {
onConnected(roomId, theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnectedEvent());
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return null;
}go deeper
Know that some values an effect reads should not cause it to re-run, and that React has a dedicated hook for reading the latest props and state from inside an effect without making them dependencies.
Explain the split it enforces: the dependency array keeps the values that define what should be synchronized, while the wrapped function holds the logic that just needs the newest values when something happens.
Show the judgment — decide per value whether a change should tear down and re-establish the effect — and name the rules: same component, called only from effects, never a prop, never a dependency, never during render.
Guard against it becoming the team's silencer for the dependency lint rule. Set the expectation that reaching for it is a signal to re-examine what the effect is actually synchronizing before the escape hatch is used.
## Reactive versus non-reactive logic in one effect An effect's dependency array is a claim: "these are the values this synchronization is *about*, and if one changes, tear down and set up again." That claim is only true for some of the values an effect body happens to read. In a chat room effect, `roomId` is genuinely reactive. It describes which connection should exist; a different room means a different connection, so re-running is correct. But the body may also call an `onConnected` callback prop and read the current `theme` to style a toast. Neither of those describes *which connection should exist*. You want their latest value at the moment the connection opens — you do not want a theme toggle to drop and re-establish a socket. The pre-19.2 options were both bad. Omitting them from the array silences the lint rule and freezes them at the render that last ran the effect, so the callback fires with stale data. Including them makes the socket churn on unrelated updates. ## What useEffectEvent gives you `useEffectEvent(fn)` returns a function that is stable across renders but whose body reads the values from the *most recent* render every time it is called. It is not memoized-with-dependencies like `useCallback`; there is no dependency list at all, and there is no staleness window. ```js import { useEffect, useEffectEvent } from 'react'; function ChatRoom({ roomId, theme, onConnected }) { const onConnectedEvent = useEffectEvent(() => { onConnected(roomId, theme); }); useEffect(() => { const connection = createConnection(roomId); connection.on('connected', () => onConnectedEvent()); connection.connect(); return () => connection.disconnect(); }, [roomId]); } ``` The array stays `[roomId]` and is now truthful: the connection is about the room and nothing else. The lint rule recognises the value returned by `useEffectEvent` and does not demand it as a dependency. ## The rules, and why each exists **Call it only from effects in the same component where it was declared.** The function has no stable value of its own to read outside a render's commit; the guarantee "latest props and state" is defined relative to the component that owns it. **Never call it during render.** It performs event-shaped work and reading it during render would make rendering impure — the same rule that keeps render functions free of side effects. **Never pass it as a prop or into another hook.** This is the rule people most want to break, because the returned function is conveniently stable. Passing it out would let a child call it at a time the owner cannot reason about, and it defeats the point: the child would be depending on something explicitly designed not to be a dependency. If a child needs a callback, pass an ordinary function. **Never put it in a dependency array.** It is not reactive by construction; listing it says the opposite. ## What it is not It is not a general escape hatch for the exhaustive-deps rule. If a value genuinely determines what should be synchronized — `roomId`, a URL, a subscription key — it belongs in the array, and wrapping the whole effect body in an effect event just to get `[]` produces an effect that sets things up once and never follows its own inputs again. The heuristic: **wrap the part that reacts to something happening; leave the part that decides what should exist.** It is also not a replacement for `useCallback`. `useCallback` produces a value you hand to other components so their memoization holds; an effect event is deliberately unshareable and exists for the inside of effects. ## Version context and the pre-19.2 workaround `useEffectEvent` shipped as a stable API in React 19.2; before that it was reachable only in experimental builds as `experimental_useEffectEvent`. On older versions the hand-rolled equivalent is a ref updated in an effect: keep `latest.current = onConnected` in an effect with no array, and call `latest.current(...)` from the main effect. That approximates the behaviour, but the ref may be read before it is updated in some orderings, and nothing stops a teammate from listing it as a dependency, which is exactly the ambiguity the dedicated hook removes. ## What an interviewer is checking Not the syntax. They want to hear you separate "values that define what should be synchronized" from "values I merely want the newest copy of when something happens", and to hear you resist using the hook to make an inconvenient dependency array go quiet. That split is the whole point of the effect-versus-event distinction, applied inside a single effect.
- Why not just wrap the whole effect body in useEffectEvent and use an empty dependency array?Because then nothing is reactive. The effect would connect once and never follow `roomId` again, so switching rooms would leave you on the old socket. Wrap only the part that reacts to something happening; the values that decide what should exist stay in the array.
- How is this different from wrapping the callback in useCallback in the parent?useCallback stabilises identity only while its own dependencies hold, so the parent's state changes still produce a new function and the effect still re-runs. An effect event is stable unconditionally and always reads the newest values, and unlike a memoized callback it is deliberately not shareable with children.
- What did people do before this hook existed?They stored the callback in a ref, updated it in an effect that runs on every render, and called `ref.current(...)` from the main effect. It approximates the behaviour but relies on effect ordering, offers no lint support, and nothing prevents someone from adding the ref to a dependency array.
saying these in an interview costs you the question
- Uses it to empty the dependency array of an entire effect
- Passes the returned function down to a child component
- Calls it during render to read the latest props
- Thinks it is just useCallback with no dependency list
- Adds the effect event to the dependency array to be safe