// PROBLEM: handler captures old state value// FIX: use updater function or ref
Example
functionBrokenCounter(){const[count, setCount]=useState(0);useEffect(()=>{const id =setInterval(()=>{// BUG: count is always 0 (captured from initial render)setCount(count +1);},1000);return()=>clearInterval(id);},[]);// empty deps = closure captures initial countreturn<p>{count}</p>;// stuck at 1}functionFixedCounter(){const[count, setCount]=useState(0);useEffect(()=>{const id =setInterval(()=>{// FIX: updater function always has the latest valuesetCount(prev => prev +1);},1000);return()=>clearInterval(id);},[]);return<p>{count}</p>;// increments correctly}
Note Stale closures happen when a callback captures a state value that never updates. Common in setInterval, setTimeout, and event listeners set up in useEffect. Use the updater form of setState or store values in a ref.
// BAD: mutating state directly
state.items.push(newItem);setState(state);// GOOD: create a new referencesetState({...state, items:[...state.items, newItem]});
Example
functionBrokenTags(){const[tags, setTags]=useState(['react','hooks']);functionaddTag(tag){// BUG: push mutates the existing array
tags.push(tag);setTags(tags);// same reference, React sees no change}return(<div>{tags.map(t =><span key={t}>{t}</span>)}<button onClick={()=>addTag('new')}>Add</button></div>);}functionFixedTags(){const[tags, setTags]=useState(['react','hooks']);functionaddTag(tag){// CORRECT: spread creates a new arraysetTags(prev =>[...prev, tag]);}return(<div>{tags.map(t =><span key={t}>{t}</span>)}<button onClick={()=>addTag('new')}>Add</button></div>);}
Note React uses reference equality (Object.is) to decide whether to re-render. Mutating an object or array in place keeps the same reference, so React skips the update. Always produce a new object/array via spread, map, filter, or slice.
state mutationmutate state directlypush state arraystate not updatingimmutable update
Missing or Bad Keys
Syntax
// BAD: index as key with reorderable list{items.map((item, i)=><Item key={i}/>)}// GOOD: stable unique identifier{items.map(item =><Item key={item.id}/>)}
Example
// BUG: using index as key with a removable listfunctionBrokenList(){const[items, setItems]=useState([{ id:'a', text:'First'},{ id:'b', text:'Second'},{ id:'c', text:'Third'},]);functionremoveFirst(){setItems(prev => prev.slice(1));}return(<div>{/* Using index: after removing first item,
React thinks items shifted, inputs lose state */}{items.map((item, index)=>(<div key={index}><input defaultValue={item.text}/></div>))}<button onClick={removeFirst}>RemoveFirst</button></div>);}// FIX: use item.id as key// {items.map(item => <div key={item.id}>...)}
Note Index keys cause broken behavior when items are added, removed, or reordered. React associates state (including uncontrolled input values) with the key. When keys shift, state gets attached to the wrong item.
key propmissing keyindex as keylist key bugwrong keykey warning
useEffect Dependency Issues
Syntax
// MISSING DEPS: effect uses stale valuesuseEffect(()=>{fn(a, b);},[]);// a, b missing// OBJECT DEPS: infinite loopuseEffect(()=>{...},[{ x:1}]);// new ref each render
Example
// BUG: missing dependencyfunctionGreeting({ name }){const[message, setMessage]=useState('');useEffect(()=>{// name is used but not in deps -- stale valuesetMessage(`Hello, ${name}!`);},[]);// should be [name]return<p>{message}</p>;}// BUG: object dependency causes infinite loopfunctionDataLoader({ filters }){useEffect(()=>{fetchData(filters);},[filters]);// if parent recreates filters object each render, infinite loop!// FIX: destructure primitive valuesconst{ category, minPrice }= filters;useEffect(()=>{fetchData({ category, minPrice });},[category, minPrice]);}
Note Enable the eslint-plugin-react-hooks exhaustive-deps rule to catch missing dependencies. For object dependencies, extract primitive values or memoize the object in the parent. Never suppress the lint rule without understanding why.
// CAUSE 1: setState during renderfunctionBad(){const[x, setX]=useState(0);setX(1);// triggers re-render during render!}// CAUSE 2: object/array in useEffect depsuseEffect(()=>{...},[{ a:1}]);// new ref every render
Example
// BUG: calling setState unconditionally during renderfunctionBrokenComponent({ data }){const[processed, setProcessed]=useState(null);// This runs on every render, which triggers a new render...setProcessed(transform(data));return<Display data={processed}/>;}// FIX: derive the value without statefunctionFixedComponent({ data }){const processed =transform(data);// compute inlinereturn<Display data={processed}/>;}// BUG: onClick={handler()} calls immediatelyfunctionBrokenButton(){const[count, setCount]=useState(0);// handler() executes during render, calling setCount, loopingreturn<button onClick={setCount(count +1)}>Click</button>;}// FIX: pass function referencefunctionFixedButton(){const[count, setCount]=useState(0);return<button onClick={()=>setCount(count +1)}>Click</button>;}
Note Infinite re-render loops crash your app. The most common causes: (1) calling setState directly in the render body, (2) onClick={fn()} instead of onClick={fn}, (3) useEffect with an object/array literal in the dependency array that creates a new reference every render.
infinite re-rendertoo many rendersrender loopmaximum update depthsetState during render
Overusing useEffect
Syntax
// ANTI-PATTERN: effect to sync derived stateuseEffect(()=>{setB(computeFrom(a));},[a]);// BETTER: compute inlineconst b =computeFrom(a);
Example
// BAD: useEffect chainfunctionPriceCalculator({ items }){const[subtotal, setSubtotal]=useState(0);const[tax, setTax]=useState(0);const[total, setTotal]=useState(0);useEffect(()=>{setSubtotal(items.reduce((s, i)=> s + i.price,0));},[items]);useEffect(()=>{setTax(subtotal *0.1);},[subtotal]);useEffect(()=>{setTotal(subtotal + tax);},[subtotal, tax]);// 3 extra renders for what should be 0!return<p>Total: ${total.toFixed(2)}</p>;}// GOOD: calculate everything inlinefunctionPriceCalculator({ items }){const subtotal = items.reduce((s, i)=> s + i.price,0);const tax = subtotal *0.1;const total = subtotal + tax;return<p>Total: ${total.toFixed(2)}</p>;}
Note Each unnecessary useEffect + setState causes an extra render cycle. If a value can be derived from props or state, compute it during render. Reserve useEffect for genuine side effects: API calls, subscriptions, DOM manipulation, and third-party library integration.
overuse useEffectuseEffect anti-patterneffect chainderived state effectunnecessary effect
Setting State After Unmount
Syntax
// PROBLEM: async callback sets state on unmounted componentuseEffect(()=>{fetchData().then(data =>setState(data));// may be unmounted!},[]);// FIX: track mounted statususeEffect(()=>{let active =true;fetchData().then(data =>{if(active)setState(data);});return()=>{ active =false;};},[]);
Example
functionUserProfile({ userId }){const[user, setUser]=useState(null);useEffect(()=>{let cancelled =false;asyncfunctionloadUser(){const response =awaitfetch(`/api/users/${userId}`);const data =await response.json();// Only update state if this effect is still activeif(!cancelled){setUser(data);}}loadUser();return()=>{
cancelled =true;};},[userId]);if(!user)return<p>Loading...</p>;return<h1>{user.name}</h1>;}
Note While React 18+ no longer warns about this, it is still a logic bug. Without the cancelled flag, a slow request for user A could resolve after switching to user B, overwriting user B's data with user A's data. Always use a cleanup flag or AbortController.
unmount state updatecancelled requestrace conditionabort fetchcleanup async
State Batching Behavior
Syntax
// React 18+ batches ALL state updates automaticallyfunctionhandleClick(){setA(1);// does not trigger rendersetB(2);// does not trigger rendersetC(3);// single render with all three updates}
Example
functionStatusPanel(){const[loading, setLoading]=useState(false);const[data, setData]=useState(null);const[error, setError]=useState(null);asyncfunctionloadData(){setLoading(true);setError(null);// React batches these two -- one rendertry{const result =awaitfetchDashboardData();setData(result);setLoading(false);// React batches these two -- one render}catch(err){setError(err.message);setLoading(false);// React batches these two -- one render}}return(<div>{loading &&<p>Loading...</p>}{error &&<p>Error:{error}</p>}{data &&<Dashboard data={data}/>}</div>);}
Note Since React 18, automatic batching applies to all updates, including those in promises, setTimeout, and native event handlers. You rarely need to worry about batching anymore. If you ever need to force a synchronous render, use flushSync (imported from react-dom) as a last resort.
state batchingmultiple setStateautomatic batchingflushSyncrender batching