React useEffect - discussion (2023-01-06)
The useEffect reference page reads simple, but the real question is always the same: what are you synchronizing, and is effect timing part of your app's contract?
How do you handle dependency arrays in a way that stays understandable as code changes? Do you have patterns for keeping effect work small (so effects don't become mini state machines)? What do you do when a feature needs 'after paint' behavior but you still want the UI to be predictable?
Comments (10)
Back to latestIf the effect depends on 9 things, it isn't a sync step anymore, it's business logic hiding in a hook.
I'd rather extract the logic and keep the effect as the thin boundary to the outside world.
We made a rule that effects must have a cleanup path, even if it's a no-op at first. It keeps people honest.
Related: Core Concepts Best Practices.
Counterpoint: sometimes you *want* a mini state machine. The trick is making it explicit and not smuggling it into an effect.
If the machine exists, name the states and render a small marker in dev builds.
For dependency arrays, we try to make dependencies stable (callbacks, stable ids) rather than suppressing warnings.
If dependencies are unstable, the effect is telling you the surface isn't stable either.
One pattern that kept our effects thin: treat them as adapters and keep the business logic in pure functions.
ts
function computeNextStatus(input: any) {
return { ready: Boolean(input), at: Date.now() };
}
useEffect(() => {
const next = computeNextStatus(input);
setStatus(next);
return () => void 0;
}, [input]);Counterpoint to "never mini state machine": sometimes the outside world *is* a state machine (sockets, subscriptions).
The key is keeping the states visible instead of letting them be implied by effect timing.
Yeah. My issue isn't the machine, it's the smuggling.
If it exists, model it explicitly and render a marker in internal builds so you can debug it.
A tiny repro of "dependencies exploded" that we see a lot:
tsx
useEffect(() => {
doThing({ a, b, c });
}, [{ a, b, c }]);
If your deps are object literals, you're basically telling React "run every time". Stabilize the shape or lift it into a document/route store.
Where effects stayed understandable for us: route boundaries.
If the effect is part of the navigation story, we put it in the shell and render evidence (lane, last-run) so it doesn't become invisible behavior.
The react.dev page is also a reminder that effects are synchronization, not computation.
If you're using an effect to "fix" state after render, you're probably fighting the model and creating phantom states.
A misunderstanding I had early on: I thought cleanup was only for subscriptions.
In practice, cleanup is also a declaration: "this effect has a lifecycle". Even a no-op cleanup makes you think about it as a contract.