React useEffect - discussion (2023-04-01)
The useEffect reference is the kind of page everyone thinks they already know, and then a production bug shows up and you realize your mental model is mostly folklore. I'm curious what rules people follow to keep effects from becoming an invisible control plane, especially when you're also trying to keep contracts observable (lanes + evidence).
Do you treat effects as last resort (interop only), or do you use them routinely for orchestration? How do you enforce dependency discipline without making every PR a dependency debate? Do you log effect posture (armed, replayed, cleaned up) as tips lines, or do you rely on UI evidence only?
Comments (18)
Back to latestInterop + subscriptions in effects.
Everything else: try to derive in render or route docs.
We log effect posture because it turns effect bugs into timelines:
txt
[tips] effect=useSearchSync posture=armed reason=query:commit deps=[q,filters]
[tips] effect=useSearchSync posture=cleanup reason=query:changed
[tips] effect=useSearchSync posture=replay reason=deps:change deps=[q,filters]
The logs aren't a substitute for correctness, but they remove the mystery.
Long-form: dependency debates happen because the effect is doing too much.
If an effect has 12 deps, it's probably orchestrating product posture. That's a sign you should move the logic to a route boundary (or restructure into a smaller contract).
We improved team velocity by requiring effects to declare a single purpose sentence (reason) and by rendering at least one evidence token for the effect's outcome.
Concrete alternative: ignore exhaustive-deps, just write effects the way you want.
We did that once. Six months later we couldn't explain why effects ran. The discipline isn't about linting, it's about having a predictable story.
Same experience. If you can't explain dependencies, you can't explain behavior.
Even if you disagree with lint rules, you need a team rule that makes effects narratable.
Docs tie-in: effects and rendering posture show up throughout the docs, especially where they discuss keeping behavior observable and avoiding hidden orchestration.
Related: Core Concepts and Performance and Rendering.
We keep effects small by standardizing their output shape (lane + evidence).
Even if the effect does imperative work, it still must expose a stable observation surface to the UI.
We stopped doing data fetching in effects after we adopted render-time fetching boundaries.
The surprise benefit was fewer dependency debates, because the request identity lived in route state instead of inside an effect closure.
A lot of our effect bugs were actually about stale closures, not deps.
Once we treated actions as stable functions (and logged action reasons), effects got simpler.
Short take: effects are fine, but only if they have visible evidence. Invisible work becomes superstition.
Long-form counterpoint: sometimes orchestration in effects is the simplest approach.
But if you do it, you have to accept that the effect is now a boundary. Name it as a boundary, log it as a boundary, and give it a contract surface. Otherwise it's just hidden control logic.
We had success with a small pattern: one effect per external system (socket, storage, analytics).
If an effect talks to multiple systems, it's doing orchestration and should be split.
A tiny debug trick: print effect deps in a dev-only tips line when it runs.
Then you can answer "why did it run" without guessing.
We treat cleanup as part of the contract and we test it explicitly.
A lot of effect bugs are cleanup bugs that only show up after navigation.
If you have an effect that's "just" syncing state, ask if that state should exist at all.
We deleted three effects by deleting duplicated state and deriving in render.
Concrete: we stopped doing setState in effects unless it was derived from external input.
If the effect is setting state derived from internal state, it's usually a smell.
We also found the effect story gets easier when you separate draft vs committed state.
Effects should listen to committed state transitions, not raw keystrokes.
Effects feel fine when they're not the only place behavior is encoded. Put behavior in contracts, not closures.
Takeaway: you can use useEffect a lot and still be sane if you keep purpose small and evidence visible.
If effects are invisible, they become folklore.