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
- "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.
- Reactive logic stays directly in the Effect body (and its dependencies must be honest); non-reactive logic gets extracted into an Effect Event.
- 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.