How to Fix AI Overusing useEffect
AI agents reach for useEffect to handle derived state, event transforms, and data fetching that should instead use useMemo, event handlers, or Server Components.
The agent wraps logic in useEffect that should be derived state, an event
handler, or a Server Component fetch — causing extra renders, stale closures,
and race conditions.
The symptom
useEffect is used to compute derived values or synchronize state that could
be a plain calculation.
"use client";import { useState, useEffect } from "react";
// WRONG — useEffect to compute derived stateexport function CartSummary({ items }: { items: CartItem[] }) { const [total, setTotal] = useState(0);
useEffect(() => { setTotal(items.reduce((sum, item) => sum + item.price * item.qty, 0)); }, [items]);
return <p>Total: ${total}</p>; // Renders twice: once with total=0, once after the effect runs}A second pattern: fetching data in useEffect when a Server Component could
do it without any client bundle cost.
"use client";useEffect(() => { fetch("/api/products").then((r) => r.json()).then(setProducts);}, []);// Waterfall: page loads -> JS executes -> fetch starts -> renderWhy it happens
useEffect was the primary side-effect escape hatch in React 16/17. Models
trained on pre-React-18 and pre-App-Router code learned to reach for it by
default. The mental model “I need something to happen after render” maps to
useEffect even when the real fix is simpler.
How to spot it
useEffectsets state that is directly computable from props or other state.useEffect(() => { fetch(...).then(setState) }, [])at the top of a component that is not inside a Server Component subtree.useEffectwith no cleanup where the dependency is a function (stale closure risk).useEffectused purely for logging or analytics on mount.
How to fix it
Use the right tool for the job:
// CORRECT — derived state: just compute itexport function CartSummary({ items }: { items: CartItem[] }) { const total = items.reduce((sum, item) => sum + item.price * item.qty, 0); return <p>Total: ${total.toFixed(2)}</p>;}
// CORRECT — expensive derivation: useMemoimport { useMemo } from "react";
export function FilteredList({ items, query }: { items: Item[]; query: string }) { const filtered = useMemo( () => items.filter((i) => i.name.toLowerCase().includes(query.toLowerCase())), [items, query] ); return <ul>{filtered.map((i) => <li key={i.id}>{i.name}</li>)}</ul>;}// CORRECT — data fetching: async Server Component (no useEffect needed)export default async function ProductsPage() { const products = await fetch("https://api.example.com/products").then((r) => r.json() ); return <ProductList products={products} />;}[ ] Derived values from props/state are plain calculations, not useEffect + setState[ ] Expensive derivations use useMemo, not useEffect[ ] Data fetching on mount moves to async Server Components or React Query/SWR[ ] Event-driven side-effects (form submit, button click) are in event handlers[ ] useEffect is reserved for: external system sync, subscriptions, cleanup[ ] Every useEffect that sets state has a loading/error state and cleanupFix Prompt
This component uses useEffect to compute derived state or fetch initial data.Refactor it: replace derived-state effects with direct calculations or useMemo,move initial data fetching to an async Server Component parent or to adata-fetching library (SWR/React Query) with proper loading/error handling, andkeep useEffect only for genuine external-system synchronization that requirescleanup. Explain each useEffect you keep and why it cannot be replaced.Test
# Count useEffect calls — a high number is a smell worth reviewinggrep -rn "useEffect" --include="*.tsx" --include="*.ts" src/ app/ \ | grep -v "node_modules" \ | wc -l
# Flag useEffect+setState patterns for manual reviewgrep -A5 "useEffect" app/**/*.tsx 2>/dev/null | grep "setState\|set[A-Z]" \ && echo "REVIEW: possible derived-state antipattern" || echo "OK"