X (formerly Twitter)

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.

Read the original on x.com ↗