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)orobj.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.
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.
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.
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.
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.
Try it yourself
- 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
Fix the sort
In the third example, change
items.sort(...)toitems.toSorted(...)(or[...items].sort(...)). Predict both console lines, then run. - 3
Count renders the pure way
Add a fourth cup to the pure version by changing
3to4in the loop. How manyrender Cuplines appear? (Eight: four cups, two calls each in StrictMode.)
Code & diagrams
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
Console
Nothing logged yet.Expected console output
[ 'Tea cup for guest #2', 'Tea cup for guest #4', 'Tea cup for guest #6' ]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
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' ]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
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.
Break #2
Push into a prop array
In a TodoList({ items }) component, write items.push("Extra") before rendering the list.
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
useMemoanduseCallbackfrom 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.innerWidthor 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 withuseSyncExternalStore(Topic 10.3).
Remember this
- 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 * 2is pure. A function that adds to a global counter, or returns a random number, is not. - 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
Props are read-only. In development React freezes the props object, so
props.title = "x"throwsTypeError: Cannot assign to read only property. But freezing is shallow: an array or object *inside* the props isn't frozen.items.sort()oruser.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()oritems.toSorted(). - 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 seecupsuntil you return it. The rule is about things that existed before the call. - 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 hasuseEffect(Phase 7), which runs after the render is committed. Use it as a last resort; most logic belongs in rendering or handlers. - 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
What makes a function pure, and why does React require components to be pure?
Why does StrictMode render components twice, and does that happen in production?
Is const list = []; list.push(x); inside a component allowed? Is props.list.push(x)? Why?
Where should side effects go if not in render?
Practice
Make this component pure:
``jsx
let total = 0;
function Price({ amount }) {
total = total + amount;
return <p>Running total: {total}</p>;
}
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.