Capítulo 39 de 116

Chapter 39: Separating Events from Effects

Core Idea

Not everything an Effect needs to read should make it re-run — event handlers are the "reactive" concept's opposite: they always see the latest values without needing to be dependencies, and useEffectEvent extends that same non-reactive-read capability into an Effect for the specific values that shouldn't trigger a re-synchronization.

Key Concepts

  • Choosing between event handlers and Effects: does this logic run because of a specific user interaction (event handler) or to stay synchronized with a value as long as the component is showing (Effect)? A message-sent analytics log belongs in the send handler; a chat-room connection belongs in an Effect.
  • Reactive values and reactive logic: props and state are reactive — code that uses them and lives inside an Effect should re-run when they change (that's the whole point of the dependency array). But some logic read inside an Effect is intentionally meant to always use the latest value without causing a re-run — e.g. reading a theme setting to decide how to show a "connected" notification shouldn't reconnect the chat room just because the user toggled dark mode.
  • The problem this creates: putting a non-reactive read directly in the Effect body forces you to either add it as a dependency (causing unwanted re-runs) or omit it (and get a stale closure/lint warning) — neither is right when the value should always be "latest" but shouldn't drive re-synchronization.
  • useEffectEvent is the fix: wrapping that non-reactive logic in useEffectEvent(fn) produces a stable function that can be called from inside the Effect, always sees the latest values when called, and — critically — is not itself a reactive dependency, so it doesn't need to be (and shouldn't be) listed in the array.
  • Effect Events are always called from inside an Effect (or another Effect Event), never passed around as a general-purpose prop/callback — they exist specifically to extract the non-reactive slice of an Effect's logic.

Code Examples

const onConnected = useEffectEvent(() => {
  showNotification('Connected!', theme); // reads latest `theme`, but not reactive
});

useEffect(() => {
  const connection = createConnection(roomId);
  connection.on('connected', () => onConnected());
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // `theme` intentionally NOT a dependency
  • What it demonstrates: theme is read inside the Effect's flow (via the Effect Event) without forcing a reconnect whenever the theme changes — only roomId drives re-synchronization.

Key Takeaways

  1. "This value should always be current, but shouldn't trigger re-synchronization" is exactly the case useEffectEvent exists for — don't solve it by lying to the dependency array instead.
  2. Reactive logic stays directly in the Effect body (and its dependencies must be honest); non-reactive logic gets extracted into an Effect Event.
  3. Effect Events are only ever called from within an Effect — they're not a general escape hatch for "give me a stable callback" outside that context.

Connects To

  • Ch 38 (Lifecycle of Reactive Effects): the reactive-dependency model this chapter carves an exception into.
  • Ch 54 (useEffectEvent): the full API reference for the Hook introduced here.
  • Ch 20 (Responding to Events): the event-handler side of the "event vs. Effect" decision this chapter opens with.