Command Palette

Search for a command to run...

Hectal
PHASE 22Advanced ~28 min· topic 7 of 7

Topic 22.7

Common React Anti-Patterns and Their Fixes

In one line

Six mistakes show up in almost every React codebase: copying props into state, chains of effects that set state, index keys on lists that change, giant components, drilling every prop everywhere, and memoising by reflex; each has a small, well-known fix taught earlier in this course.

Think of it like this

A doctor's handbook of common illnesses. A good doctor doesn't invent a cure for every cough; they recognise the pattern ("that's a cold"), know the standard treatment, and know which symptoms mean something else. An anti-pattern is a common solution that looks reasonable but causes problems later. Learning to recognise them on sight, with their standard fix, is one of the fastest ways to get better at React and at code review.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Anti-pattern
A common solution that seems reasonable but causes problems; it has a known better alternative.
Mirrored state
State that copies a prop, so it stops following the prop after the first render.
Derived value
A value you can calculate from props or state; it doesn't need its own state.
Effect chain
Effects that set state which triggers other effects, causing extra renders and in-between states.
Stable key
A key taken from the data (like an id) that stays the same for the same item across renders.
Colocation
Keeping state in the lowest component that needs it.
Premature optimisation
Making code more complex for speed before measuring that it's slow.

Step by step

01How to spot them in a code review

Each anti-pattern has a visible signature. Searching for these shapes finds most of them in minutes.

review checklistwhole filebash
useState(props.x)  or useState(x) where x is a prop      -> mirrored state? (6.2, 6.4)
useEffect(() => { setY(f(x)) }, [x])                       -> derived value or effect chain (7.6)
key={index} / key={i} on a list that can change          -> index keys (3.3)
> 300 lines, > 6 useState, several unrelated effects        -> giant component (22.3, 6.6)
props passed through 3+ components that don't use them    -> drilling: use composition (9.1)
memo(...) / useMemo / useCallback with no profile          -> premature memo (16.3, 16.6)

02Giant component: before

Everything lives in Dashboard: the filters, the data loading, the chart and the table. Typing one letter in the search box re-renders the chart and the table.

Dashboard.jsx (before, shortened)whole filejsx
function Dashboard() {
  const [search, setSearch] = useState("");
  const [range, setRange] = useState("7d");
  const [sort, setSort] = useState("date");
  const [rows, setRows] = useState([]);
  const [status, setStatus] = useState("loading");
  const [chartType, setChartType] = useState("bar");
  // ...9 more useState, 4 effects, 300 lines of JSX with nested ternaries
}

03Giant component: after

Data loading moves into a hook, each panel owns its own local state, and Dashboard only arranges the panels. Typing in the search box now re-renders only SearchBox's subtree.

Dashboard.jsx (after)whole filejsx
function Dashboard() {
  const [range, setRange] = useState("7d");          // shared by both panels: stays here
  const report = useReport(range);                  // loading, errors, caching
  return (
    <Layout toolbar={<RangePicker value={range} onChange={setRange} />}>
      <ChartPanel data={report.data} />              {/* owns chartType */}
      <TablePanel data={report.data} />              {/* owns search and sort */}
    </Layout>
  );
}

04Prop drilling: before and after composition

Before, Page and Sidebar pass user along only so Avatar can read it. After, the top component builds the Avatar itself and passes it down as a slot; the middle layers just place it.

drilling vs compositionwhole filejsx
// before
<Page user={user} />
function Page({ user }) { return <Sidebar user={user} />; }
function Sidebar({ user }) { return <nav><Avatar user={user} /></nav>; }

// after
<Page sidebar={<Sidebar avatar={<Avatar user={user} />} />} />
function Page({ sidebar }) { return <div className="page">{sidebar}</div>; }
function Sidebar({ avatar }) { return <nav>{avatar}</nav>; }
Prop drilling: before and after compositiondiagram
Rendering diagram…

05Fix one at a time, with a test

Refactoring anti-patterns changes behaviour in subtle ways (a reset that used to happen now doesn't). Write or keep a behaviour test (Phase 17) for the screen, fix one anti-pattern, run the test, commit. Linting helps too: eslint-plugin-react-hooks 7 flags setting state synchronously in an effect and other patterns from this list.

Try it yourself

  1. 1

    The deliberate copy

    In the first example, rename the broken component's prop to initialName and pass initialName={user.name}. Does the behaviour change? No, but now a reader knows the copy is intentional. That naming convention is the documented escape hatch for "prop as starting value".

  2. 2

    Count the renders

    In the effect-chain example, add a third effect that sets shippingCost from freeShipping. Predict how many broken render lines one click now prints, then check.

  3. 3

    Fix memo the other way

    In the broken page, move { color: "teal" } into a constant outside the component, as the fixed page does. Predict how many times ChartBroken renders while typing. Then ask: is the memo still needed if the state moves down?

Code & diagrams

Mirrored props: broken versus reset with a key New tab

The two components are identical. The only difference is `key={user.id}` on the fixed one: a new key makes React throw the old state away and start again from the new prop. The components are the same because the fix is in **how the parent uses it**.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Selected Ben -> broken shows Asha | fixed shows Ben
An effect chain versus calculating during render New tab

One click on the broken cart renders three times, and two of those renders show a wrong state (60 in the cart, but `total` 40; then `total` 60 but no free shipping). The fixed cart renders once with consistent values, and has two fewer state variables to keep in sync.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

broken render: total 40 free false
fixed render: total 40 free false
--- click both
broken render: total 40 free false
broken render: total 60 free false
broken render: total 60 free true
fixed render: total 60 free true
Index keys versus stable ids when deleting a row New tab

Both lists deleted Milk. With index keys, React kept the components for keys 0 and 1 and dropped key 2, so the Eggs row now holds Milk's note: the input's own state stayed with the position, not with the item. With `item.id` keys the right row is removed.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

broken: Eggs / note: Milk | Rice / note: Eggs
fixed: Eggs / note: Eggs | Rice / note: Rice
Premature memo versus moving state down New tab

The broken page pays for `memo` and `useMemo` and still re-renders the chart on every keystroke, because `{ color: "teal" }` is a new object each time. The fixed page has no memoisation at all, and the chart never re-renders: the typing state lives in `SearchBox`, which isn't the chart's parent.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

ChartBroken rendered
Chart rendered
--- typing 3 letters in each
ChartBroken rendered
ChartBroken rendered
ChartBroken rendered

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Fixing mirrored state with another effect

Fix GreetingBroken by adding useEffect(() => { setName(user.name); }, [user]);.

terminal
$ Preview
── what you'll see ──
After switching user, the input first renders 'Asha' for one frame, then 'Ben'. Typing works, but every time the parent re-renders with a new user object (same person), the user's unsaved typing is wiped.

Break #2

Using Math.random() or Date.now() as a key

Fix index-key warnings by writing key={Math.random()} on each row.

terminal
$ Preview
── what you'll see ──
Every keystroke anywhere on the page clears every note input in the list and focus jumps out of the field being typed in.

Myth vs fact

Myth

Copying a prop into state is always wrong.

Fact

It's right when the prop is a starting value for something the user edits. Name it initialX or defaultX, and reset with a key when the source changes.

Myth

Index keys are always wrong.

Fact

They're fine for static lists that never reorder, filter, insert or delete. They break as soon as items move relative to their position.

Myth

memo everywhere is a free performance win.

Fact

It costs a props comparison and memory on every render, adds noise, and is defeated by any new object or function prop. Profile first, fix structure first.

Myth

Context is the cure for prop drilling.

Fact

Composition (children, slot props) is usually the first cure. Context is for widely read values, and overusing it makes components hard to reuse and re-renders readers.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    eslint-plugin-react-hooks 7 ships rules that catch several of these (for example set-state-in-effect for synchronous setState inside an effect, and purity for Math.random() during render); they come from the React Compiler's analysis and are in the recommended preset.

  • ▸

    Storing "previous props" in state and adjusting during render (if (prevUser !== user) { setPrevUser(user); setName(user.name); }) is the documented last-resort pattern when a key can't be used; React re-runs the component immediately without committing the stale version.

  • ▸

    A useful code-review heuristic for giant components: count reasons to change (data source, layout, a business rule, an animation). More than two or three unrelated reasons in one component is the signal to split, not the line count alone.

  • ▸

    Many anti-patterns are symptoms of wrong state placement (Topic 6.6): mirrored state (state in the child that belongs to the parent), drilling (state too high), unnecessary memo (state too high, re-rendering big subtrees). Moving state to the right place often fixes several at once.

Remember this

  1. 1

    1. Mirroring props in state. const [name, setName] = useState(user.name) copies the prop once, on the first render. When the parent later passes a different user, the state keeps the old value and the screen lies. Fixes (Topic 6.2 and 6.4): if you only display it, use the prop directly or calculate from it during render; if it's a draft the user edits, keep the state but make the parent reset it with key={user.id}; if you need both, name the prop initialName so the copy is clearly deliberate.

  2. 2

    2. Effect chains. An effect that sets state, which triggers another effect that sets more state, renders the component again for every link of the chain, shows inconsistent values in between (total updated but freeShipping not yet), and is hard to follow. Fix (Topic 7.6): values derived from state are calculated during render; changes caused by a user action happen in the event handler, all at once. Effects are only for synchronising with things outside React.

  3. 3

    3. Index keys on changing lists. key={index} tells React "the first row is always the same row". Delete the first item and React keeps the first row's component (with its state, its input text, its focus) and gives it the second item's data. Fix (Topic 3.3): use a stable id from the data (key={item.id}). Index keys are fine only for lists that never reorder, insert or delete.

  4. 4

    4. Giant components. One 800-line component with 15 useStates, five effects and nested ternaries is hard to read, re-renders everything for every keystroke and can't be tested in pieces. Fix: split along real seams. Pull logic into custom hooks (Topics 10.1 and 22.3), pull repeated or independent UI into child components, and move state down to the smallest component that needs it (colocation, Topic 6.6), which also means fewer components re-render.

  5. 5

    5. Prop drilling everywhere, and context everywhere. Passing ten props through five layers that don't use them makes every middle component change when the leaf does. The first fix is composition (Topic 9.1): pass finished JSX as children or slot props so the layout in the middle never sees the data. Context (Topic 9.2) is for values many distant components read, such as the theme or the current user. The opposite anti-pattern, one giant context for everything, re-renders every reader on every change (Topic 15.2).

  6. 6

    6. Premature memoisation. Wrapping every component in memo and every value in useMemo/useCallback adds code and memory, and often does nothing: one inline object prop (options={{ color: "teal" }}) breaks memo on every render (Topic 16.3). Fix: measure first (Topic 16.6); prefer structural fixes (move state down, lift constant objects out of the component, pass children); add memo only where the Profiler shows a slow render. With React Compiler (Topic 16.5), most manual memoisation is unnecessary.

Explain it without notes

01

Why is copying a prop into state a bug, and what are the fixes?

02

What is wrong with a chain of effects that set state, and how do you refactor it?

03

Why do index keys cause bugs, and when are they acceptable?

04

How would you approach memoisation in a React codebase?

Practice

01

Refactor: function Price({ amount, rate }) { const [converted, setConverted] = useState(0); useEffect(() => { setConverted(amount * rate); }, [amount, rate]); return <span>{converted.toFixed(2)}</span>; }

02

This list uses key={i} and each row has a checkbox with local state. Items can be sorted. What breaks, and how do you fix it?

Trade-offs

  • ↔

    Refactoring anti-patterns improves correctness and readability but changes behaviour in subtle places (resets, timing); do it one at a time behind behaviour tests.

  • ↔

    Some anti-patterns are acceptable in throwaway code or tiny components; the cost appears when the code grows or other people reuse it.

Done when you can

  • I can spot mirrored state and fix it by deriving or resetting with a key.

  • I can replace an effect chain with values calculated during render and logic in handlers.

  • I can explain and demonstrate the index-key bug and use stable ids.

  • I can split a giant component with hooks, child components and state colocation.

  • I can fix prop drilling with composition before reaching for context.

  • I can decide when memoisation is worth it and prefer structural fixes first.