useEffectEvent

Read the Latest Value Without Re-Running the Effect

useEffectEvent

`useEffectEvent` (still in canary at time of writing) carves the non-reactive part out of an effect — useful when you want to read fresh state without including it in deps.

4 min read Level 3/5 #react#useEffect#useEffectEvent
What you'll learn
  • Recognize when an effect dep "should not" trigger re-runs
  • Use `useEffectEvent` to separate reactive setup from non-reactive logic

A common pain point: your effect reads some state, but you don’t want changes to that state to re-run the effect.

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const conn = connect(roomId);
    conn.on("message", msg => showToast(theme, msg));
    return () => conn.disconnect();
  }, [roomId, theme]);  // theme change → reconnects? Probably not what we want
}

theme is only used for the visual toast — it shouldn’t cause a reconnection. But omitting it from deps means stale closures.

The Hook

useEffectEvent(fn) returns a stable function that always sees the latest props/state when called, but isn’t itself reactive.

function ChatRoom({ roomId, theme }) {
  const onMessage = useEffectEvent(msg => {
    showToast(theme, msg);   // reads latest theme
  });

  useEffect(() => {
    const conn = connect(roomId);
    conn.on("message", onMessage);
    return () => conn.disconnect();
  }, [roomId]);   // theme NOT in deps — it's encapsulated by useEffectEvent
}

Now theme is read fresh each time onMessage is called, but changing theme does NOT cause the effect to tear down and re-run.

When To Reach For It

The signal is: “I have an effect dep that’s only used in a callback, and listing it forces unnecessary reconnections/re-fetches”.

If the value affects setup (the URL, the connection key, the subscription parameters), it belongs in deps. If it only affects side reactions (toasts, logging, analytics), wrap them in useEffectEvent.

Status

useEffectEvent is the modern answer to a very common problem and is shipping in React. If your version doesn’t have it yet, the workaround is a ref:

const themeRef = useRef(theme);
useEffect(() => { themeRef.current = theme; }, [theme]);

useEffect(() => {
  const conn = connect(roomId);
  conn.on("message", msg => showToast(themeRef.current, msg));
  return () => conn.disconnect();
}, [roomId]);

Same idea — refs aren’t reactive, so the effect doesn’t re-run when theme changes.

Chapter Done

That’s every built-in hook plus custom hooks. Next chapter: patterns and the rest of React 19’s toolkit.

Composition Over Configuration →