Topics in this subject
React 4 min read Updated 5 Aug 2026

Gotchas & Anti-patterns

Common React mistakes — keys, mutation, needless effects, stale closures — each with the fix.

Quick map#

flowchart TD
  A["Symptom"] --> B["Wrong list order / lost input state"]
  B --> C["key={index} — use a stable id"]
  A --> D["Update doesn't render"]
  D --> E["Mutated state — create new object/array"]
  A --> F["Extra renders / bugs"]
  F --> G["Unnecessary useEffect — derive or use handlers"]

1. key={index}#

Index keys break when the list reorders/inserts/deletes — React reuses the wrong DOM node, corrupting input state and animations. ✅ Use a stable unique id from your data: key={item.id}. Index is only OK for static, never-reordered lists.

2. Mutating state#

state.push(x) / state.foo = 1 doesn't change the reference, so React may skip the re-render, and it corrupts time-travel/StrictMode. ✅ Produce a new value: setItems([...items, x]), setUser({ ...user, foo: 1 }). (Or use Immer.)

3. Unnecessary useEffect 🎯#

An effect to compute derived data from props/state causes an extra render and can desync. ✅ Derive during render for computed values; use event handlers for user-driven side effects.

// ⚠️ effect syncing derived state
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);

// ✅ derive during render
const full = `${first} ${last}`;

4. Stale closures#

A callback captures the values from the render it was created in. With useEffect(fn, []) or an event handler set once, you read old state. ✅ Use the functional updater setCount(c => c + 1), or add the value to the deps array, or use a ref for the latest value.

// ⚠️ always reads the initial count
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);

// ✅ functional update — no stale capture
useEffect(() => {
  const id = setInterval(() => setCount((c) => c + 1), 1000);
  return () => clearInterval(id);
}, []);

5. Duplicating props in state#

const [x, setX] = useState(props.x) snapshots the prop — later prop changes are ignored. ✅ Use the prop directly, or lift state up. If you truly need local editable copy, that's a deliberate "uncontrolled from prop" pattern — reset via key.

6. The 0 && render bug 🎯#

{count && <List/>} renders 0 when count is 0 (a number, not falsy-hidden like false/null). ✅ Coerce to boolean: {count > 0 && <List/>} or {!!count && ...} or ternary {count ? <List/> : null}.

7. Huge context re-renders#

Every consumer re-renders when the context value changes; passing a fresh object each render (value={{ user, setUser }}) makes it worse. ✅ Split contexts (state vs dispatch), memoize the value with useMemo, or move to a store with selectors.

8. Missing cleanup#

Effects that start subscriptions/timers/listeners without a cleanup leak and cause "set state on unmounted" work. ✅ Return a cleanup function; abort fetches with AbortController.

useEffect(() => {
  const ctrl = new AbortController();
  fetch(url, { signal: ctrl.signal }).then(/* ... */);
  return () => ctrl.abort();   // ✅ cleanup on unmount / dep change
}, [url]);

9. Over-memoizing#

useMemo/useCallback/memo everywhere adds allocation + dependency-tracking cost and clutters code, often with no measurable win. ✅ Memoize only when profiling shows it: expensive computations, referential stability for memoized children, or large lists. (React 19's Compiler auto-memoizes, reducing manual need.)

10. Conditional / looped hooks#

Calling hooks inside if/loops/early-returns breaks the rules of hooks and desyncs state. ✅ Call hooks unconditionally at the top level; put conditions inside the hook.

11. Index-based derived state & unstable list identity#

Storing selected-index instead of selected-id breaks when the list changes. ✅ Track by id, not position.

Anti-pattern cheat table#

Anti-pattern Fix
key={index} Stable key={item.id}
state.push(x) setItems([...items, x])
Effect to derive value Compute during render
setCount(count+1) in stale closure setCount(c => c + 1)
useState(props.x) Use prop / lift state
{count && <X/>} {count > 0 && <X/>}
One giant context Split + useMemo value
No effect cleanup Return cleanup / AbortController
Memoize everything Memoize where profiled

Interview Q&A#

Q1. Why is key={index} a problem? On reorder/insert/delete the index no longer maps to the same item, so React reconciles against the wrong element — corrupting input state, focus, and animations. Use a stable id.

Q2. Give an example of an unnecessary useEffect. Syncing derived state, e.g. an effect that sets fullName from first/last. Derive it during render instead; effects for computed data add a render and risk desync.

Q3. What's a stale closure and how do you avoid it? A callback captures state from its creation render; with empty deps it keeps reading old values. Fix with functional updaters (setX(prev => ...)), correct deps, or a ref holding the latest value.

Q4. Why does {count && <List/>} render 0? 0 is falsy but React renders the number 0 rather than nothing. Guard with an explicit boolean: {count > 0 && <List/>} or a ternary.

Q5. When should you memoize? Only when it pays off: expensive computations, stable references for React.memo children, or big lists — verified by profiling. Blanket useMemo/useCallback adds overhead; React 19's Compiler reduces the manual need.