React Ban useEffect
Direct useEffect is an external-synchronization escape hatch. It lives in a
reviewed, domain-specific hook for a real external system; components stay
effect-free.
Scope
Effect replacement changes the touched behavior. Repository-wide lint enforcement is a separate task: apply it when requested or required by existing repository policy. A review request produces findings; an implementation request authorizes in-scope replacements and verification without another approval checkpoint.
Workflow
-
Find direct imports,
React.useEffect(...)namespace calls, and wrappers arounduseEffectin the touched surface. -
Classify each effect and replace it in this order of preference:
- render-time calculation
- server, loader, or framework data API
- the repository's server-state layer (for example a TanStack Query
queryOptionsfactory with variables in the key and thesignalpassed through) - event handler, action, or mutation
- keyed component boundary
useSyncExternalStore- reviewed domain-specific hook for a real external system
If none fits, state the external synchronization contract before keeping an effect. Replacements covers the non-obvious cases.
-
Preserve the repository's framework, data layer, lint shape, and dependency policy. Add a library only when the request or repository policy authorizes it; without a server-state layer, call the remaining effect a design gap.
-
Run lint, type, and test gates plus the smallest real UI or hook check for the changed behavior.
Legitimate Effects
Put every legitimate effect in a hook named for its external system (a chat
connection, a map SDK, a widget), in its own file. The hook owns setup and
cleanup, takes its reactive inputs as parameters, and lists every value setup
or cleanup reads, so react-hooks/exhaustive-deps passes unsuppressed. Prefer
an existing repository hook.
The hook is specific to one external system. A generic wrapper that takes a
callback and a caller-provided dependency array (useUpdateEffect(fn, deps),
useMountEffect(fn)) is a direct effect under another name, and server state,
prop copies, user actions, and keyed resets each have their own replacement
above.
Enforcement
When enforcement is in scope, extend the repository's existing linter so it
rejects both named useEffect imports and React.useEffect(...) calls. Allow
only the named, reviewed integration-hook files, not a directory such as
src/hooks/**; report generic wrappers and their callers as violations to
migrate. Run the same lint surface in the canonical local gate and CI, with
pinned tool versions.
In review, every new direct effect is a finding unless the change documents and proves a legitimate external-system boundary. Ask for a replacement, not dependency-array tuning.
Boundaries
- Scope migrations to the touched or requested surface.
- Leave
useLayoutEffect,useInsertionEffect, framework lifecycle APIs, and non-React effect systems alone, including in lint rules, unless requested. - Apply performance primitives only for measured behavior or an established repository pattern.
- Upstream attribution: upstream.