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.