Gotchas & Anti-patterns
Common React mistakes — keys, mutation, needless effects, stale closures — each with the fix.
🧑🏫 Sabse pehle — simple mein samjho#
Ye woh aam galtiyan hain jo React mein har naye developer se ho jaati hain — jaise ghar ka MCB baar-baar trip ho, chhoti si cheez par app atak jaaye. Sabse common: list mein key={index} lagana (Flipkart cart se item hatao toh galat item ka data reh jaata), state ko seedha badalna (React ko pata hi nahi chalta ki kuch change hua, screen update nahi hoti), aur bina zaroorat useEffect daalna. Inhe pehchano aur bacho — code saaf aur bug-free rahega. Neeche do chhote example dekho: galat vs sahi.
// ❌ Galat — state ko seedha mutate kiya, re-render nahi hoga
cart.push(item);
setCart(cart);
// ✅ Sahi — naya array banao
setCart([...cart, item]);
// ❌ Galat — list mein index ko key banaya
{items.map((it, i) => <Row key={i} {...it} />)}
// ✅ Sahi — stable id use karo
{items.map((it) => <Row key={it.id} {...it} />)}
Yaad rakho: state ko naya banao (mutate mat karo), aur key hamesha stable id ho — index nahi.
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.