Synchronizing with Effects - discussion (2022-08-20)
Effects are supposed to be for synchronization, but we keep using them as a place to patch app state after the fact.
What effect patterns do you actually like in real apps (subscriptions, DOM, timers) and what do you ban? Do you have conventions that make effects debuggable (naming, rendered signals, strict cleanup discipline)? How do you migrate a sync effect into an explicit contract (route keys, derived evidence)?
Comments (10)
Back to latestTimers and listeners, fine. Effects that fix state after render are always a regret.
We treat effects like tiny state machines with a rendered signal: start, active, cleanup.
It's not to spam logs, it's to make timing visible in screenshots and route-flow tests.
Related: Testing and Debugging and Resilience Drills (Deep Dive #3).
We standardized a helper and it made reviews easier (it returns a status you can render):
tsx
function useNamedEffect(name: string, fn: () => void | (() => void), deps: unknown[]) {
const [status, setStatus] = useState<'start' | 'active' | 'cleanup'>('start');
useEffect(() => {
setStatus('start');
const cleanup = fn();
setStatus('active');
return () => {
setStatus('cleanup');
cleanup?.();
};
}, deps);
return { name, status } as const;
}
// usage
const effect = useNamedEffect('presence:socket', () => subscribe(), [roomId]);
return <div data-effect={effect.name} data-status={effect.status} />;Counterpoint: I worry signals normalize effects by making them feel safe.
The best fix is still deleting the effect and making the contract explicit. Signals are a bandage, not a design.
I agree. The signals are mostly for the effects we can't avoid (subscriptions, analytics).
For sync effects, the goal is to migrate to route keys and derived evidence.
That framing helps. If it's unavoidable external sync, make it visible. If it's internal sync, delete it.
The worst is URL-sync effects. They always fight back/forward. If it should be shareable, it should be route truth.
Cleanup discipline matters more than deps for us.
A leaky subscription is the kind of bug you can feel but can't explain without something visible.
We had an effect that would settle derived state after navigation and it caused flicker only on back/forward. Deleting it and storing the derived evidence in the doc fixed both flicker and explainability.
tsx
const doc = useRouteDoc('inbox');
const threadId = doc.read('threadId');
const derived = useMemo(() => buildDerived(threadId), [threadId]);
doc.write('derivedEvidence', derived);
return <div data-thread={threadId} data-derived="stored" />;If you keep an effect, name it. If you can't name it, you can't review it.
Strict Mode doubling effects is a great audit. Anything that breaks was relying on timing, not contracts.
If an effect changes behavior and nothing in the UI can prove it happened, you're debugging a ghost.