Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~22 min· topic 6 of 7

Topic 2.6

Props Are Read-Only: Keeping Components Pure

In one line

A React component must behave like a pure function: given the same props (and state), it returns the same JSX and changes nothing outside itself while rendering; side effects go in event handlers or effects, and StrictMode renders twice in development to catch components that break this rule.

Think of it like this

A recipe. Follow the recipe for one cake with the same ingredients and you get the same cake, every time, and following it doesn't change the recipe book or empty your neighbour's fridge. A pure component is a recipe: the same props in, the same JSX out, and nothing else in the house changes because you read it.

Words you'll meet

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

Pure function
A function that changes nothing that existed before it ran and always returns the same result for the same inputs.
Side effect
Any change the outside world can notice: changing outside variables, the DOM, the network, storage, timers.
Mutation
Changing an existing object or array in place, like arr.push(x) or obj.name = "x".
Local mutation
Changing a variable or array you created inside this render. It's safe, because nothing else can see it yet.
Read-only
You may look at it but not change it. Props are read-only.
Event handler
A function React calls when the user does something, like a click. Side effects are allowed there.
StrictMode
A development helper that renders components twice (and more checks) to expose impure code.
Idempotent
Doing it twice gives the same result as doing it once. Pure renders are idempotent.

Step by step

01Spot the outside variable

This component changes guest, a variable that lives outside it. Each call gives a different result, so it isn't pure. Calling it three times shows #1, #2, #3, which looks right by luck.

Cup.jsxwhole filejsx
let guest = 0;

function Cup() {
  guest = guest + 1;           // changes something outside: impure
  return <h2>Tea cup for guest #{guest}</h2>;
}

02Turn on StrictMode and watch it break

StrictMode calls every component twice. The outside variable goes up twice per cup, so you see #2, #4, #6. The bug was always there; StrictMode made it visible.

Turn on StrictMode and watch it breakdiagram
Rendering diagram…

03Pass the data in as props

Make guest an input instead of outside memory. Now Cup depends only on its props. Called once or twice, <Cup guest={2} /> always shows #2.

Cup.jsxwhole filejsx
function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

function TeaGathering() {
  const cups = [];                      // created in this render
  for (let i = 1; i <= 3; i++) {
    cups.push(<Cup key={i} guest={i} />); // local mutation: fine
  }
  return cups;
}

04Don't mutate props, copy them

items.sort() sorts the array in place, which is the parent's array. Copy first.

Ranked.jsxwhole filejsx
function Ranked({ items }) {
  // items.sort(...)  changes the parent's array: impure
  const sorted = items.toSorted((a, b) => b - a); // new array: pure
  return <p>Ranked: {sorted.join(", ")}</p>;
}

05Move side effects out of render

Saving, fetching, logging analytics and changing the page title are side effects. Do them in an event handler when the user acts, or in an effect after render when they must happen because the component is on screen.

Move side effects out of renderdiagram
Rendering diagram…

Try it yourself

  1. 1

    Remove StrictMode

    In the first example, render <TeaSet /> without <StrictMode>. Predict the cups. (#1, #2, #3.) Why is that more dangerous than the StrictMode result?

  2. 2

    Fix the sort

    In the third example, change items.sort(...) to items.toSorted(...) (or [...items].sort(...)). Predict both console lines, then run.

  3. 3

    Count renders the pure way

    Add a fourth cup to the pure version by changing 3 to 4 in the loop. How many render Cup lines appear? (Eight: four cups, two calls each in StrictMode.)

Code & diagrams

StrictMode exposes an impure component New tab

This example uses StrictMode on purpose (most examples on Hectal leave it out). StrictMode called each `Cup` twice, so the outside counter rose by two each time and the cups show #2, #4 and #6. Remove `<StrictMode>` and you'd see #1, #2, #3: the bug hides, but it's still there.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 'Tea cup for guest #2', 'Tea cup for guest #4', 'Tea cup for guest #6' ]
The pure version survives the double render New tab

Also in StrictMode on purpose. Every `Cup` still renders twice (each log line appears twice), but the screen is correct, because a pure component gives the same answer both times.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

render Cup 1
render Cup 1
render Cup 2
render Cup 2
render Cup 3
render Cup 3
[ 'Tea cup for guest #1', 'Tea cup for guest #2', 'Tea cup for guest #3' ]
Mutating a prop changes the parent's data New tab

The first `Entered` rendered before `Ranked` sorted the array; the second rendered after, so the same data shows two different orders on one screen. The fix is one word: `items.toSorted(...)` returns a new array and leaves the prop alone.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 'In the order entered: 30, 10, 20', 'Ranked: 30, 20, 10', 'In the order entered: 30, 20, 10' ]
scores is now [ 30, 20, 10 ]

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

Assign to a prop

Inside a component, write props.title = "Changed"; before returning.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot assign to read only property 'title' of object '#<Object>'
An error occurred in the <Card> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

Break #2

Push into a prop array

In a TodoList({ items }) component, write items.push("Extra") before rendering the list.

terminal
$ Preview
── what you'll see ──
"Extra" appears once, then twice, then more each time the list re-renders (twice at once under StrictMode). The parent's array keeps growing.

Myth vs fact

Myth

StrictMode's double render is a React bug that slows my app.

Fact

It's a deliberate development-only check, and production renders once. If double rendering changes what you see, your component is impure.

Myth

Pure components can't use variables or loops.

Fact

Creating and changing local variables during render is fine. Purity is about not changing anything that existed before the render.

Myth

console.log in a component makes it impure, so it's not allowed.

Fact

Strictly a log is a side effect, but a harmless one used for debugging. Just expect it to print twice under StrictMode.

Myth

Props are frozen, so I can't accidentally change them.

Fact

Only the top-level props object is frozen. Arrays and objects inside props can still be mutated, which changes the parent's data.

Pro corner

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

  • ▸

    Purity is what makes React's features possible: concurrent rendering can start rendering, pause for urgent input, and throw the work away; Suspense may render a tree, discard it and retry; server rendering runs components where there is no DOM. All of this is safe only if rendering has no side effects.

  • ▸

    In React 19, StrictMode's second render reuses the memoised results of useMemo and useCallback from the first call, and React DevTools can dim the console logs from the second call. In development, StrictMode also runs effects setup → cleanup → setup and (React 19) calls ref callbacks twice, to catch missing cleanups (Topic 7.4).

  • ▸

    The React Compiler (Topic 16.5) memoises components automatically, and it relies on the rules of React: components and hooks must be pure, props and state treated as immutable. Code that mutates during render can be compiled into stale UI, which is why the compiler's ESLint rules flag it.

  • ▸

    Reading changing outside values during render is impure too: Date.now(), Math.random(), window.innerWidth or a global store read directly. They give different answers on each render and can cause hydration mismatches with server rendering (Topic 20.6). Read them in event handlers or effects, or subscribe properly with useSyncExternalStore (Topic 10.3).

Remember this

  1. 1

    In programming, a pure function has two properties. It minds its own business: it doesn't change any object or variable that existed before it was called. Same inputs, same output: give it the same arguments and it always returns the same result. double(x) => x * 2 is pure. A function that adds to a global counter, or returns a random number, is not.

  2. 2

    React assumes every component is pure while rendering. Rendering means React calling your function to find out what to show; it may do that at any time, many times, in any order, or start and throw the result away. So, during render, a component must not: change its props, change variables or objects created outside it, change the DOM directly, start a network request, set a timer, or write to localStorage. Anything like that is a side effect: a change the outside world can notice.

  3. 3

    Props are read-only. In development React freezes the props object, so props.title = "x" throws TypeError: Cannot assign to read only property. But freezing is shallow: an array or object *inside* the props isn't frozen. items.sort() or user.name = "x" on a prop changes the parent's data in place, and every other component that shares it sees the change. Make a copy instead: [...items].sort() or items.toSorted().

  4. 4

    Local mutation is fine. A variable or array you create *during this render* belongs to this render alone. const cups = []; cups.push(<Cup />) inside the component is perfectly pure, because nobody else can see cups until you return it. The rule is about things that existed before the call.

  5. 5

    Side effects are not forbidden, they just have a place. Event handlers (onClick, onSubmit) run when the user does something, not during render, so they may change state, call APIs or write to storage (Topic 4.1). For effects that must happen because a component *appeared on screen*, such as connecting to a chat server, React has useEffect (Phase 7), which runs after the render is committed. Use it as a last resort; most logic belongs in rendering or handlers.

  6. 6

    Why StrictMode renders twice. <StrictMode> (Topic 0.4) calls each component function twice in development and keeps one result. A pure component gives the same answer both times, so you never notice. An impure one shows its bug immediately: a counter that jumps by two, a list that doubles, a log line that appears twice. Production builds render once. The live example below shows a component that changes an outside variable, caught by StrictMode.

Explain it without notes

01

What makes a function pure, and why does React require components to be pure?

02

Why does StrictMode render components twice, and does that happen in production?

03

Is const list = []; list.push(x); inside a component allowed? Is props.list.push(x)? Why?

04

Where should side effects go if not in render?

Practice

01

Make this component pure: ``jsx let total = 0; function Price({ amount }) { total = total + amount; return <p>Running total: {total}</p>; }

02

This component reverses its prop. Fix it so the parent's array is never changed: ``jsx function Newest({ posts }) { return <ul>{posts.reverse().map((p) => <li key={p}>{p}</li>)}</ul>; }

Trade-offs

  • ↔

    Copying arrays and objects instead of mutating them costs a little memory and time, which is almost always negligible next to the bugs it prevents. For very large data, immutable helper libraries such as Immer make copying cheaper to write (Topic 4.6).

  • ↔

    StrictMode's double calls make logs noisier and can surprise beginners, but turning it off only hides bugs. Keep it on and make components pure.

Done when you can

  • I can say what a pure function is in two rules.

  • I never assign to props or mutate arrays and objects inside them.

  • I know local mutation during render is fine.

  • I know side effects belong in event handlers or effects.

  • I can explain why StrictMode renders twice and what it reveals.