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
Mutating state directly
Directly mutating state doesn't tell React anything has changed, so it won't re-render. Always produce a new value (object, array) 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, no re-render guaranteed// 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, wrap it in useMemo. The React Compiler can also handle this for you.
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 — depend on `count`, or use a functional updateruseEffect(() => {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 in <StrictMode> the dev-time double-mount makes the leak immediately visible (two intervals, two listeners).
// 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 is unsafe — render must be pure, and React 19's strictness flags this. Read/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 ref = useRef(0);useEffect(() => {ref.current += 1;});return <div>{ref.current}</div>;}
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 only matters when the receiving child is wrapped in React.memo and the new reference defeats the memoization. With the React Compiler enabled, even those cases are usually handled for you.
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.
The React Compiler (RC/stable as of 2026) handles most of this automatically, making manual memoization unnecessary in many codebases.
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'],});