At Factory, we have a simple, yet important frontend rule: no useEffect. Yes, it sounds strict. In practice, it has made our codebase easier to reason about and much harder to accidentally break.
What we mean by "banned"?
We never call useEffect directly. For the rare case where you need to sync with an external system on mount, we have useMountEffect().
Most useEffect usage is compensating for something React already gives you better primitives for: derived state, event handlers, and data-fetching abstractions.
This matters even more now that agents are writing the code. useEffect is often added 'just in case,' but that move is the seed of the next race condition or infinite loop. Banning the hook forces the logic to be declarative and predictable.
Hard lesson
We did not arrive at this rule so easily. We got there through production bugs.
Compounding problems
Brittleness: Dependency arrays hide coupling. A refactor that seems unrelated can quietly change effect behavior.
Infinite loops: It is easy to create state update -> render -> effect -> state update loops, especially when dependency lists get "fixed" incrementally.
Dependency hell: Effect chains (A sets state that triggers B) are time-based control flow. They are hard to trace and easy to regress.
Debugging pain: You end up asking "why did this run?" or "why did this not run?" without a clear entrypoint like a handler.
A cultural meme
At this point, useEffect is a running joke across the React community.
Official React team
This is not just our internal preference. React has a full guide called You Might Not Need an Effect.
The problem? useEffect shifted many teams from explicit event-driven logic to implicit synchronization logic. Instead of reacting to a clear event, you are managing relationships between values and side effects through dependency arrays.
The solution
Below are the five patterns that replaced most of our useEffect usage.
Rule 1: derive state, do not sync it
Most effects that set state from other state are unnecessary and add extra renders.
This pattern also creates loop hazards:
Smell test:
You are about to write useEffect(() => setX(deriveFromY(y)), [y])
You have state that only mirrors other state or props
Rule 2: use data-fetching libraries
Effect-based fetching often creates race conditions and duplicated caching logic.
Smell test:
Your effect does fetch(...) and then setState(...)
You are re-implementing caching, retries, cancellation, or stale handling
Rule 3: event handlers, not effects
If a user clicks a button, do the work in the handler.
Smell test:
State is used as a flag so an effect can do the real action
You are building "set flag -> effect runs -> reset flag" mechanics
Rule 4: useMountEffect for one-time external sync
useMountEffect is just useEffect(..., []) wrapped in a named hook to make intent explicit and prevent ad-hoc effect usage in components.
Good uses:
DOM integration (focus, scroll)
Third-party widget lifecycles
Browser API subscriptions
A useful pattern is conditional mounting.
Smell test:
You are synchronizing with an external system
The behavior is naturally "setup on mount, cleanup on unmount"
Rule 5: reset with key, not dependency choreography
If the requirement is "start fresh when ID changes," use React's remount semantics directly.
Smell test:
You are writing an effect whose only job is to reset local state when an ID/prop changes
You want the component to behave like a brand-new instance for each entity
Forcing function for nesting
Banning direct useEffect works as a forcing function for cleaner tree design. Parents own orchestration and lifecycle boundaries. Children can assume preconditions are already met. You get simpler components and fewer hidden side effects.
This is basically Unix philosophy applied to React components: each unit does one job, and coordination happens at clear boundaries.
Choose your bug
No team ships zero bugs. The question is which failure mode you want.
useMountEffect failures are usually binary and loud (it ran once, or not at all). Direct useEffect failures often degrade gradually and show up as flaky behavior, performance issues, or loops before a hard failure.
You should too
After operating a no-direct-useEffect codebase on a large app, we saw fewer infinite loops, fewer race-condition regressions, and faster onboarding because control flow became easier to follow.
The rule felt extreme at first. It now feels like a baseline engineering guardrail.
How to adopt this rule
Enforce this with lint rules, clear agent guidance in AGENTS.md. To mass fix existing use cases, try our new Missions product to mass fix each violation. You can use this post as a reference guide for your droids.