The real job of useEffect
useEffect is for synchronizing your component with something outside React: a subscription, a browser API, a timer, a mutation of external state, or a network side effect. It is not the right place to repair ordinary render logic that could be derived directly during render.
When teams misuse useEffect, they end up suppressing dependencies, storing values they could derive, and writing callbacks that read stale props or state. The code works just enough to survive a demo, then falls apart under real interactions.
Where useEffectEvent fits
useEffectEvent solves a narrow but painful problem. Sometimes an effect should remain stable, but the callback triggered inside that effect still needs the freshest props or state. That is exactly where useEffectEvent wins.
A subscription callback is the clearest example. You want to set up the subscription once for a given dependency, but the callback invoked by that subscription may need the latest UI values. useEffectEvent lets you keep the effect stable without lying to the dependency list.
- Use useEffect when the effect itself should re-run because one of its true external dependencies changed.
- Use useEffectEvent when the effect should stay stable, but the callback invoked from inside the effect must read the latest state or props.
- Do not use useEffectEvent as a shortcut to avoid understanding why an effect is re-running.
Common anti-patterns to remove during migration
The most common smell is a comment or eslint disable that exists only to keep a callback out of the dependency array. Another is an effect that sets local state from values that already exist in props or memo-free render logic.
If you see either pattern, stop before adding another hook. First ask whether the effect should exist at all. If it should, ask whether the effect is stable but the callback is stale. Only then is useEffectEvent the right tool.
A practical migration path
Start with effects that manage subscriptions, analytics listeners, or long-lived browser integrations. Those are the highest-value candidates because they often require stable setup plus fresh reads inside callbacks.
Then test the behavior that matters: reconnect logic, repeated navigation, and rapid prop changes. If the hook change makes the code quieter but the tests do not get stronger, you probably only moved the confusion around.