React State Management Anti-Patterns
📂 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.
// ❌ 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:
- The
useEffectcauses an extra render — React renders once whenorderschanges, then again after the effect setstotal totalis not independent state — it's entirely derived fromorders, making it redundant- It adds unnecessary indirection to what should be a simple calculation
The Fix: Compute Directly in Render
// ✅ 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 foruseMemoimmediately. 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.
// ❌ 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
// ✅ 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:
| Category | Hook | Re-render? |
|---|---|---|
| Values that affect the UI | useState | ✅ Yes |
| Internal values with no UI impact | useRef | ❌ 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.
// ❌ 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
// ✅ 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:
- Is there a
useEffectsetting this state? → Likely derived state. Compute it directly in render. - Is there no setter being used? → The
useStateis useless. Use aconstor move it outside the component. - 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.
// ❌ 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-Pattern | Root Cause | Fix |
|---|---|---|
useState + useEffect for derived values | Duplicating computable data | Calculate directly in render |
useState for non-UI values | Wrong hook choice | Use useRef |
| Storing full objects instead of IDs | Redundant data copies | Store 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.