Keeping Components Pure
React assumes every component is a pure function: same inputs → same JSX, and it touches nothing that existed before it ran. This isn't a style preference — a lot of React's machinery only works because it can count on this.
In plainer words: a component's only job while rendering is to look at its inputs and return what to show. It shouldn't change anything outside itself along the way. Give it the same inputs and it should draw the same thing every time — like a calculator, not a diary.
The two rules of a pure render
A render should behave like double(n) => 2 * n — predictable, and minding its own business.
- Same inputs, same output. Given the same props/state/context, it returns the same JSX.
- No changing things from the outside. It doesn't mutate anything that existed before the render.
Here's the classic way it breaks — reaching for a variable declared outside:
let guest = 0;
function Cup() {
guest = guest + 1; // mutating something outside render
return <h2>Tea cup for guest #{guest}</h2>;
}
Render three cups and you don't reliably get 1, 2, 3 — the output depends on how many times React happened to call it. The fix is to make the value an input instead:
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
Props, state, context are read-only during render
While rendering, I treat all three as read-only. If I feel the urge to reassign a prop or mutate a state object mid-render, that's the signal I actually want an event handler or an effect.
Local mutation is completely fine
The rule is about pre-existing things. Anything I create inside the render, I can mutate freely — no one else can see it yet.
function TeaGathering() {
const cups = []; // created in this render
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />); // mutating my own local array — fine
}
return cups;
}
Side effects live outside render
Rendering calculates the JSX; anything that changes the world — updating state, hitting an API, starting an animation, writing to the DOM — happens elsewhere. Usually in an event handler (runs on a user action, not during render), and only as a last resort in useEffect (runs after render).
function Counter({ count, setCount }) {
// no side effect while rendering — it's wired to a click
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
StrictMode calls every component twice in development on purpose. A pure function returns the same thing both times, so nothing changes. An impure one (like the guest counter above) produces doubled or scrambled output — which is exactly the point: the double-render surfaces the impurity instead of letting it hide.
The test I actually use
Would anything be wrong if React ran this render twice, or skipped it?
It comes down to one question: does the render write to something that existed before it started? Location of the code is irrelevant — location of the thing being changed is everything.
Impure — writing to something pre-existing, or not deterministic. Each line has a different failure:
user.name = user.name.toUpperCase(); // edits the PARENT's object — other components see the change
cart.items.push(newItem); // React won't notice (same reference) → UI silently stale
todos.push(item); // same trap: mutating a prop, no re-render, corrupts parent data
document.title = text; // touches the outside world; skipped/replayed renders break it
const now = Date.now(); // same inputs, different output → React can't cache or reuse it
Pure — reading anything, but only writing to what this render just made:
const price = amount * (1 + TAX_RATE); // reading an outside constant
const sorted = [...todos].sort(byOrder); // new array, then mutate the copy
let n = 0; items.forEach(() => { n += 1; }); // local counter, resets next render
The tell: the moment I want the forbidden thing — a value that changes, a DOM write, the time on a click — that's the signal for state + an event handler, not the render body.
Why React insists on this
Purity is what buys React its freedom to be clever: it can skip re-rendering a component whose inputs didn't change (safe caching), interrupt and restart a render mid-tree without corrupting anything, and render the same component on the server for many requests. Break purity and every one of those optimizations becomes unsafe.