You've been lying to your dependency array. And it's time to stop.
We've all done it. You need to read a value inside useEffect, but you don't want the effect to re-run when that value changes. So you omit it. You suppress the lint warning. You feel dirty.
useEffectEvent is the confession booth that absolves you of these sins.
The Problem: React's reactivity model is "all or nothing". If you use a variable, it's a dependency. If it changes, the effect runs. But often, we want to read state without reacting to it.
Imagine a chat app. You want to show a notification when a message arrives. You need the current theme color to style the notification.
If you add theme to the dependencies, the effect runs every time the user toggles Dark Mode, showing the notification again. That's a bug.
If you remove theme, the linter yells at you, and you risk stale closures.
02. Meet useEffectEvent
// The Old Way (Buggy or Hacky)
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', (msg) => {
// ๐ด Re-runs connection if 'theme' changes!
showNotification(msg, theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // ๐ We don't want 'theme' to restart the connection!
// The New Way (Clean & Correct)
const onMessage = useEffectEvent((msg) => {
// โ
Read latest 'theme' without becoming a dependency
showNotification(msg, theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('message', (msg) => {
onMessage(msg);
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // โ
'theme' is gone! No restarts.
useEffectEvent creates a special function that is stable (it never changes identity, so you don't need to put it in dependencies) but always has access to the latest props and state.
It essentially "extracts the non-reactive logic" out of your reactive Effect.
03. Logic vs. Reactivity
The Mental Model Split
-
๐
Reactive Code (useEffect): "When Room ID changes, I must re-connect."
This code must run to keep the app synchronized with the world. -
๐ง
Non-Reactive Logic (useEffectEvent): "When a message arrives, I check the Theme."
This code only runs in response to an event, using whatever the state describes at that moment.
04. Real World: Analytics Logging
A classic scenario: You want to log "Page Visited" when the route changes. You also want to include the current `user.id` and `cart.total` in the log payload.
If you put `cart.total` in the dependency array, you will log "Page Visited" every time the user adds an item to the cart!
function PageLogger({ route, user, cart }) {
// 1. Define the event handler with access to latest state
const logVisit = useEffectEvent((currentRoute) => {
analytics.logEvent('page_view', {
route: currentRoute,
userId: user.id, // โก๏ธ Access latest user
cartVal: cart.total // โก๏ธ Access latest cart
});
});
// 2. Trigger it only when route changes
useEffect(() => {
logVisit(route);
}, [route]); // โ
Triggers ONLY on route change
}