What are some React anti-patterns?
TL;DR
React anti-patterns are practices that lead to inefficient, buggy, or hard-to-maintain code. Common ones in modern (hooks-era) React include:
- Mutating state directly instead of producing a new value
- Using
useStateto mirror props or other state instead of computing the value during render - Using
useEffectto derive data that could just be computed - Using array index as
keyfor dynamic lists - Stale closures inside effects (missing or wrong dependencies)
- Forgetting to clean up effects (subscriptions, timers, listeners)
- Mutating refs during render
- Not using keys in lists at all
- Reaching for
useMemo/useCallbackeverywhere instead of where they actually help
Common React anti-patterns
Most React anti-patterns violate one of three ideas: renders should be pure, state should have one owner, and identity should remain stable for the same logical item.
Mutating state directly
Directly mutating state breaks React's snapshot model. Passing the same object back also makes the update eligible for an Object.is bailout, so React may not re-render for the mutation. Always produce a new value and pass it to the setter.
// Anti-patternconst [user, setUser] = useState({ name: 'Ada', age: 36 });user.age = 37; // mutation — React doesn't see thissetUser(user); // same reference; React can ignore this update// CorrectsetUser((prev) => ({ ...prev, age: 37 }));
Using state to mirror props or other state
A common anti-pattern is copying a prop into state in order to "have a local copy." This usually leads to two sources of truth that drift out of sync.
// Anti-pattern — `fullName` mirrors props in statefunction Greeting({ firstName, lastName }) {const [fullName, setFullName] = useState(`${firstName} ${lastName}`);// fullName won't update when firstName/lastName change!return <h1>{fullName}</h1>;}// Correct — compute it during renderfunction Greeting({ firstName, lastName }) {const fullName = `${firstName} ${lastName}`;return <h1>{fullName}</h1>;}
If the derived value is genuinely expensive, measure it and consider useMemo. React Compiler may provide equivalent memoization when enabled.
Using useEffect to derive data
If a value can be computed from existing props/state, don't store it in state and update it from an effect — just compute it during render. The effect version adds an extra render, can flash stale values, and is harder to reason about.
// Anti-patternfunction Cart({ items }) {const [total, setTotal] = useState(0);useEffect(() => {setTotal(items.reduce((sum, i) => sum + i.price, 0));}, [items]);return <div>{total}</div>;}// Correctfunction Cart({ items }) {const total = items.reduce((sum, i) => sum + i.price, 0);return <div>{total}</div>;}
The React docs have a full guide on this: You Might Not Need an Effect.
Using array index as key on dynamic lists
key={index} is fine if the list is static and never reordered. As soon as items can be inserted, removed, or reordered, index keys cause React to reuse the wrong DOM nodes and component state — leading to subtle bugs (text inputs keeping the wrong value, animations playing on the wrong row).
// Anti-pattern for a list that can changeitems.map((item, i) => <Row key={i} item={item} />);// Correctitems.map((item) => <Row key={item.id} item={item} />);
Not using keys in lists at all
Omitting key triggers a console warning and forces React to fall back to position-based reconciliation, which is the same problem as index keys. Always use a stable, unique key.
// Anti-patternitems.map((item) => <li>{item.name}</li>);// Correctitems.map((item) => <li key={item.id}>{item.name}</li>);
Stale closures in effects
When an effect captures a value but doesn't list it in the dependency array, it keeps reading the old value forever. This shows up as "the timer keeps logging 0," "the WebSocket sends old form values," etc.
// Anti-pattern — effect closes over `count` but doesn't depend on ituseEffect(() => {const id = setInterval(() => {console.log(count); // always logs the initial value}, 1000);return () => clearInterval(id);}, []);// Correct — re-synchronize the interval when `count` changesuseEffect(() => {const id = setInterval(() => {console.log(count);}, 1000);return () => clearInterval(id);}, [count]);
Let eslint-plugin-react-hooks's exhaustive-deps rule catch these.
Forgetting to clean up effects
Subscriptions, timers, intervals, and event listeners need to be torn down in the Effect's cleanup function. Otherwise they leak—and at a Strict Mode root, the extra development setup/cleanup cycle makes missing cleanup easier to notice.
// Anti-patternuseEffect(() => {window.addEventListener('resize', onResize);}, []);// CorrectuseEffect(() => {window.addEventListener('resize', onResize);return () => window.removeEventListener('resize', onResize);}, []);
Mutating refs during render
Reading or writing ref.current during render, except for predictable one-time initialization, breaks render purity and can make component behavior unpredictable. The current eslint-plugin-react-hooks refs rule reports many such cases. Read or write refs in event handlers and Effects instead.
// Anti-patternfunction Component() {const ref = useRef(0);ref.current += 1; // side effect during renderreturn <div>{ref.current}</div>;}// Correctfunction Component() {const clickCount = useRef(0);function handleClick() {clickCount.current += 1;alert(`Recorded clicks: ${clickCount.current}`);}return <button onClick={handleClick}>Record click</button>;}
Inline functions and objects in JSX
Defining a function or object inline (onClick={() => doThing(id)}, style={{ color: 'red' }}) creates a fresh reference each render. This is not automatically a performance problem — for the vast majority of components it's the idiomatic style, and the React docs explicitly say not to optimize prematurely. It matters when identity affects a memoized child or a Hook dependency. React Compiler can memoize many compatible cases when enabled.
Prop drilling and the over-correction into context
Passing a prop through five layers that don't use it is annoying, but the fix isn't always context. Often the right answer is to lift state to the right place, compose components differently (children, slots), or pull in a state library for genuinely global state. Wrapping every shared value in context invites the context pitfalls — every consumer re-renders on every change.
Overusing useMemo and useCallback
Memoization isn't free — it costs memory plus the equality check on every render. Sprinkling useMemo/useCallback on every value "just in case" usually loses time rather than saving it. Reach for them when:
- The wrapped computation is genuinely expensive, or
- The value is passed to a
React.memo'd child or a hook dependency array where reference stability matters.
React Compiler 1.0 is an optional build-time optimizer that memoizes compatible code automatically. It can reduce manual memoization, but profiling and clear dependency semantics still matter.
Deeply nested state
Deeply nested state is awkward to update immutably and easy to get wrong. Prefer a flatter shape, but don't lose information that the structure encoded. The fix below loses the "users have profiles" grouping; a better fix keeps the relationship while flattening the tree:
// Anti-pattern — deeply nestedconst [state, setState] = useState({user: {profile: {name: 'John',age: 30,},},});// Correct — flatten while preserving structureconst [user, setUser] = useState({ name: 'John', age: 30 });// Or, for collections, normalize by idconst [users, setUsers] = useState({byId: {u1: { id: 'u1', name: 'John', age: 30 },},allIds: ['u1'],});
Further reading
- You Might Not Need an Effect
- Choosing the State Structure
- Rendering Lists — why does React need keys?
useMemo— should you add it everywhere?- React Compiler