React useEffect - discussion (2022-11-05)
We keep re-litigating what effects are “for” and the disagreements usually come down to: are we using effects to synchronize with external systems, or are we patching state after render because we didn’t define the contract clearly enough?
What effect patterns do you still allow in 2022 (subscriptions, analytics, DOM) and which ones do you aggressively delete? Do you have conventions that make effect behavior visible from the UI (status markers, last-run evidence) instead of hiding it in logs? How do you migrate an effect-driven sync into a route contract key / derived evidence story?
Comments (10)
Back to latestIf it’s internal state sync, delete it. If it’s external system sync, keep it but make it boring and visible.
The rule that helped us: effects must either (a) integrate with the outside world or (b) write evidence the UI can render.
If it does neither, it’s just secret work.
Related: Testing and Debugging and Using Effects to Keep State Consistent.
We added a visible effect marker so timing bugs stopped being ghosts:
tsx
function useEffectMarker(name: string, deps: unknown[]) {
const [status, setStatus] = useState<'start' | 'active' | 'cleanup'>('start');
useEffect(() => {
setStatus('start');
setStatus('active');
return () => setStatus('cleanup');
}, deps);
return { name, status } as const;
}
const e = useEffectMarker('presence:socket', [roomId]);
return <div data-effect={e.name} data-status={e.status} />;Counterpoint: markers can normalize bad designs.
I’d rather see teams delete the effect and store derived evidence than become “good” at writing effects.
I agree in principle. The markers are for the effects we can’t delete (subscriptions, analytics).
For internal sync effects, the marker is a temporary tool to prove what’s happening until we migrate to contract keys.
That’s a good distinction: unavoidable sync vs “we didn’t decide the contract”.
The effects that hurt us most were URL sync effects.
Once we treated URL as hint and route state as truth, the effect disappeared and so did the weird back button behavior.
We migrated a “fix state after render” effect by writing the derived value once in the route doc:
tsx
const doc = useRouteDoc('inbox');
const derived = useMemo(() => derive(state), [state]);
doc.write('derivedEvidence', derived);
return <div data-derived="stored" />;Strict Mode double-invoking effects is a great audit for “is this effect actually safe”.
Anything that breaks was relying on timing, not contracts.
Link: Strict Mode.
Cleanup discipline is the difference between “we have one subscription” and “we have 9 subscriptions and nobody knows why”.
If your effect changes behavior and nothing in the UI can prove it happened, you’ll debug it by reading code every time.
Effects aren’t evil. Invisible effects are.
Make them boring, scoped, and visible — or delete them and move the decision into the route contract.