This article is published in English.
Retiring the Latest-Value Ref: Effect Events with useEffectEvent
Learn how useEffectEvent in React 19.2 separates non-reactive logic from an effect, removes latest-value ref hacks, and when the hook is the wrong tool.
Many React codebases contain a useRef whose only job is to hold "the latest value" so an effect does not have to depend on it. The trick works, but it hides intent. React 19.2 makes useEffectEvent stable, giving that pattern a first-class hook. By the end you will be able to split reactive and non-reactive logic inside an effect, and know when the hook does not apply.
The dependency array that cannot win
The conflict appears with long-lived connections: chat rooms, WebSocket feeds, analytics sessions, subscriptions. A ChatRoom receives roomId and theme. The effect must reconnect when roomId changes, but it reads theme only to style a toast after connecting.
Add theme to the dependency array and switching to dark mode tears down the socket for no reason. Leave it out and the hooks lint rule complains; silence it and the callback keeps the theme captured by an old render, a classic stale closure.
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // stale after theme changes
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme shouldn't trigger a reconnect
}
The workaround most teams shipped
The usual fix was to copy theme into a ref on every render and read ref.current inside the callback. It works, but the synchronization step is easy to forget, and nothing in the code says the logic is deliberately non-reactive.
Splitting event logic out with useEffectEvent
useEffectEvent extracts the non-reactive part of an effect into its own function. That function always sees the latest props and state, calling it never re-runs the effect, and it does not go into the dependency array.
Below, the notification moves into onConnected. The effect only creates the connection, registers the handler, connects, and disconnects on cleanup.
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always fresh, never a dependency
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // only roomId matters now
}
Only roomId remains as a dependency. theme has not been hidden from the linter; it has been reclassified. Showing a toast when a connection opens is a reaction to an event, so it is event logic rather than synchronization. Toggling the theme no longer reconnects, and the next toast still uses the current theme.
Conceptually this is what the ref hack did by hand: React keeps the function you call from the effect pointing at the latest render's closure, and the intent is now visible.
What you gain
- No more stale closures. The Effect Event reads the latest values without manual ref updates.
- Fewer pointless re-runs. The effect fires only when a value it truly synchronizes with changes.
- A clearer mental model. Effects mean "keep this system in sync with this state"; Effect Events mean "do this when that happens".
The React docs have urged developers to separate events from effects since the React 18 era; 19.2 finally backs that advice with a stable hook. For the rest of the release, see our overview of React 19.2's Activity, useEffectEvent and static rendering.
Before you reach for it
useEffectEvent does not replace useEffect, and it is not a general way to shrink dependency arrays.
- Use it only when a change in the value should not restart the effect. If it should, the value belongs in the array.
- Never call an Effect Event during rendering; invoke it from inside effects, such as subscription callbacks or timers.
- Keep it local to its component instead of passing it to children as a prop.
- If most of an effect ends up inside an Effect Event, the logic probably belongs in a regular event handler.
Before 19.2 the hook existed only in experimental builds, so projects on 19.0 or 19.1 must upgrade to use the stable API.
Key takeaways
- Every "keep the latest value in a ref" workaround is a candidate for
useEffectEvent. - For each dependency, ask whether a change should restart the effect; if not, the code reading it is event logic.
- The API is tiny, but it names a problem almost every React developer has hit without knowing what to call it.