React State Management Anti-Patterns

5 min read

📂 Exercise: exercise-antipatterns/page.tsx


Anti-Pattern #1: Deriving State with useEffect 🎯

The Problem

A very common pattern in React codebases: using a useState + useEffect combination where the effect exists solely to compute and set a new state value.

TSX
// ❌ Anti-pattern: Deriving state via useEffect const [orders, setOrders] = useState<Order[]>([]); const [total, setTotal] = useState(0); useEffect(() => { setTotal(orders.reduce((sum, o) => sum + o.price * o.quantity, 0)); }, [orders]);

Why this is wrong:

  1. The useEffect causes an extra render — React renders once when orders changes, then again after the effect sets total
  2. total is not independent state — it's entirely derived from orders, making it redundant
  3. It adds unnecessary indirection to what should be a simple calculation

The Fix: Compute Directly in Render

TSX
// ✅ Correct: Derive state directly in render const [orders, setOrders] = useState<Order[]>([]); const total = orders.reduce((sum, o) => sum + o.price * o.quantity, 0);

On useMemo: Don't reach for useMemo immediately. Start by computing directly in render. Only memoize if you have a measured performance problem.

The rule: If a value can be computed from existing state or props, it is derived state calculate it in render, don't store it.


Anti-Pattern #2: Using useState for Non-Render Values

The Problem

useState triggers a re-render every time it's updated. Using it for values that have no effect on the like a timer ID or scroll position tracker causes unnecessary renders.

TSX
// ❌ Anti-pattern: Timer ID in useState const [timerId, setTimerId] = useState<NodeJS.Timeout | null>(null); const startTimer = () => { const id = setTimeout(() => { /* ... */ }, 1000); setTimerId(id); // triggers a re-render — but nothing in the UI changed };

The Fix: Use useRef for Internal Values

TSX
// ✅ Correct: Timer ID in useRef const timerIdRef = useRef<NodeJS.Timeout | null>(null); const startTimer = () => { timerIdRef.current = setTimeout(() => { /* ... */ }, 1000); // no re-render triggered }; const stopTimer = () => { if (timerIdRef.current) { clearTimeout(timerIdRef.current); timerIdRef.current = null; } };

The rule: Component state has two categories:

CategoryHookRe-render?
Values that affect the UIuseState✅ Yes
Internal values with no UI impactuseRef❌ No

useRef is not just for DOM elements — use it for any value that should persist across renders without causing them.


Anti-Pattern #3: Redundant State (Storing Full Objects) 💡

The Problem

Storing a full object in state when only an ID is needed creates redundant data the same information exists in two places. The selected object becomes a snapshot that can go stale.

TSX
// ❌ Anti-pattern: Storing the full selected hotel object const [hotels, setHotels] = useState<Hotel[]>([]); const [selectedHotel, setSelectedHotel] = useState<Hotel | null>(null); const handleSelect = (hotel: Hotel) => { setSelectedHotel(hotel); // copies the entire object };

Why this is dangerous: If hotels is refreshed from an API (price update, availability change), selectedHotel is now stale it no longer reflects the current data.

The Fix: Store the ID, Derive the Object

TSX
// ✅ Correct: Store only the ID, derive the full object const [hotels, setHotels] = useState<Hotel[]>([]); const [selectedHotelId, setSelectedHotelId] = useState<string | null>(null); // Derived — always fresh from the source of truth const selectedHotel = hotels.find(h => h.id === selectedHotelId) ?? null; const handleSelect = (hotelId: string) => { setSelectedHotelId(hotelId); };

Now selectedHotel is always derived from the current hotels array any update to the list is automatically reflected.


Exercise Patterns: Quick Reference

The exercise-antipatterns file covers all three patterns. Here's what to look for:

Spot the Anti-Pattern Fast

Search your codebase for useState. Then ask:

  1. Is there a useEffect setting this state? → Likely derived state. Compute it directly in render.
  2. Is there no setter being used? → The useState is useless. Use a const or move it outside the component.
  3. Is this a full object that exists elsewhere in state? → Store only the ID instead.

Static Values Outside Components

If a value is truly static (e.g., a user profile that never changes during the session), it doesn't belong in useState at all. Reference it directly or pass via props/context.

TSX
// ❌ Redundant: Static value in useState with no setter const [userName] = useState(userProfile.name); // ✅ Correct: Just reference it directly const userName = userProfile.name;

Note: Avoid defining static values as module-level closures if your component needs to be portable and testable. Prefer props or context so the function stays self-contained.


Summary: The Single Source of Truth Rule

All three anti-patterns violate the same principle single source of truth.

Anti-PatternRoot CauseFix
useState + useEffect for derived valuesDuplicating computable dataCalculate directly in render
useState for non-UI valuesWrong hook choiceUse useRef
Storing full objects instead of IDsRedundant data copiesStore ID, derive the object

The more code you can delete while keeping the same behavior, the better. These refactors are some of the most satisfying in React.