Ban direct `useEffect` in React components. Use when writing, refactoring, or reviewing React code so derived state, data fetching, user actions, resets, and mount-only external synchronization use declarative replacement patterns instead of dependency-array choreography.
72
90%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Use this when a React task touches direct useEffect. Classify the effect by intent; then replace it with the smallest pattern that preserves behavior.
React's own guidance frames effects as an escape hatch for synchronizing with systems outside React. Factory's no-direct-useEffect rule applies the same idea as an agent guardrail: hidden synchronization through dependency arrays is easy for agents to add and hard for humans to trace later.
Common failure modes:
Use when an effect sets state from props, state, URL params, query params, feature flags, or selectors.
Smells:
useEffect(() => setX(f(y)), [y])Prefer:
const filteredProducts = products.filter((product) => product.inStock);For expensive pure calculations, use useMemo only after the repo's performance expectations justify it. React Compiler may remove some need for manual memoization, so do not add useMemo reflexively.
Use when an effect fetches data and stores it in component state.
Smells:
fetch(...).then(setState) inside an effectignore, cancelled, or stale response flagsPrefer, in repo order:
When using TanStack Query, prefer queryOptions, query keys that include variables, enabled for dependent queries, select for server-state derivation, AbortSignal-aware query functions, mutations with invalidation, and optimistic updates with cancellation/rollback where appropriate.
Use regular useQuery when dependent enabled gates, placeholder pagination, or cancellation semantics matter. TanStack v5 Suspense query hooks intentionally do not support all regular-query options.
Do not introduce a new dependency without explicit approval. If the repo has no server or server-state layer, state that the direct effect is a design gap and keep the smallest local fix honest.
Use when an effect waits for state to change so it can run work caused by a click, form submit, keyboard action, drag, or route interaction.
Smells:
setShouldSubmit(true) then an effect posts dataPrefer:
async function handleSubmit() {
await saveDraft(formState);
navigate("/drafts");
}In React 19 or framework code that supports form actions, prefer the repo's action pattern plus useActionState, useFormStatus, or useOptimistic for pending/error/optimistic UI. useFormStatus belongs in a child component rendered inside the form. Call useOptimistic updates from an action or transition. In TanStack Query code, use useMutation and invalidate or update the relevant query keys. Share duplicated behavior by extracting a function that event handlers call directly. Do not put the shared behavior in an effect just to avoid a helper.
Use when an effect clears or reloads local state after an identity prop changes.
Smells:
useEffect(() => setComment(""), [userId])Prefer a keyed boundary:
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}When only selection needs adjusting, prefer storing stable IDs and deriving selected objects during render. Updating state during render is a last resort and must be tightly guarded.
Use useMountEffect only when the work is naturally setup on mount and cleanup on unmount. For external systems that must resync when props or state change, use a reviewed explicit exception or dependency-aware wrapper instead of pretending the work is mount-only.
Good candidates:
useSyncExternalStorePrefer useSyncExternalStore when the component reads a changing external store or browser value. That gives React a subscription and snapshot contract instead of a hand-rolled effect that copies external state into local state.
Before using it, try conditional mounting:
function VideoPlayerContainer({ isReady }: { isReady: boolean }) {
if (!isReady) return null;
return <VideoPlayer />;
}
function VideoPlayer() {
useMountEffect(() => {
const player = startPlayback();
return () => player.stop();
});
}This keeps preconditions in the parent and lifecycle in the child.
Use when an effect exists only to debounce, delay, copy, or stage render work after input changes.
Smells:
Prefer:
useDeferredValue when a slow child can lag behind an urgent inputuseTransition or framework navigation pending state for non-urgent updatesSuspense boundaries for async UI that can stream or reveal independentlyPromise.all or framework parallel data APIs for independent requestsUse these only when the changed path has visible latency, React Doctor findings, profiler evidence, or an existing repo convention. Do not scatter performance hooks as decoration.
useEffect import remains in touched React components.React.useEffect(...) namespace calls remain in touched React components.useEffect import/call exceptions.useSyncExternalStore when they expose changing values.key, component boundaries, or render-time derivation.