You Might Not Need an Effect - discussion (2023-01-19)
This page keeps showing up in reviews, and I'm trying to pin down the practical boundary between "effects are fine" and "this should be derived state or a boundary choice". In this repo's vocabulary, it feels like the advice is really about making the render contract explicit and storing derived keys where siblings need coherence.
What are your most common effect deletions and what replaces them (derived keys, keyed remount, explicit reset)? Where does this advice fail for you (analytics, subscriptions, imperative integrations)? How do you make the remaining effects observable so behavior isn't purely timing-based?
Comments (16)
Back to latestKeyed remount replaced most of our "sync draft to id" effects.
It felt crude at first, but it eliminated an entire class of stale state bugs.
We also replaced "sync props to state" with derived keys written in the mutation path:
ts
function applyMutation(doc: any, next: any) {
doc.write('source', next);
doc.write('derivedPreview', buildPreview(next));
doc.write('isValid', validate(next));
}
If the derived preview is needed for rendering, it shouldn't arrive after render in an effect.
Counterpoint: sometimes the effect is the simplest and the replacement is worse.
We kept a couple of effects but added evidence for them (status + lastRunAt) so they weren't invisible.
Same. The article gets weaponized as "effects forbidden" sometimes.
For me it's more: if you *must* use an effect, it should be readable and observable.
Explicit events are underrated.
If a user action should update two keys, do it in the handler/mutation path rather than waiting for an effect to "catch up".
We added a debug panel that lists current effects by name and status (running, cleanup, error).
That small UI made effect bugs much less spooky.
Most common deletion for us: "reset selection when filter changes" effect.
Instead, we made selection self-correct in render based on visible ids, and we stored visible ids as derived state.
A misunderstanding I had: I thought the alternative to effects is "compute in render".
Sometimes the alternative is "store derived keys so render is a cheap read".
Counterpoint: storing derived state adds a maintenance burden if mutations aren't centralized.
If you can't guarantee derivations run, maybe render computation is safer even if it's slower.
Agree. Storing derived keys is only stable if there's a disciplined mutation path or reconcile step.
Otherwise it's a trap.
We kept effects for analytics and subscriptions, but we made them observable with a lastEvent evidence string.
It made it possible to debug without attaching a profiler.
Docs tie-in: this is basically the same posture as the data fetching docs: behavior should be legible and deterministic.
Related: Data Fetching and Caching and State Management (Best Practices).
We also adopted a rule: effects can't write "source of truth" keys, only evidence/status keys.
It kept us from accidentally building a second state machine in effects.
The best replacement for "sync" effects was making identity changes explicit (route/session slugs) and using remount boundaries.
Once identity is explicit, state doesn't need to be "corrected" after render.
If you're trying to delete effects, start by listing effects that exist to align two pieces of state.
Those are usually the ones where a single mutation path or a derived key makes the most sense.
The page is a good reminder that effects are expensive mostly because they're invisible. Make them visible or replace them.
We used to have an effect that enforced UI policy (disable button after submit).
We replaced it with a stored derived canSubmit key so the UI state was always coherent in render.
The advice works best when you pair it with a discipline: centralize writes, store derived evidence, and keep effects for outside-world sync.