Keeping Components Pure
A React component is supposed to behave like a pure function in math: given the same props and state, it always returns the same JSX, and it does not change anything outside of itself while figuring that out. This idea is called keeping components pure, and it is one of the most important — and most frequently broken — rules in React. Purity is what lets React safely render components more than once, render them in any order, skip re-rendering them, or even pause and resume rendering without your UI going wrong.
This lesson explains exactly what “pure” means for a component, why React depends on it under the hood, and the specific mistakes that quietly break it in real apps.
Overview / How it works
In plain JavaScript, a pure function has two properties: it does not change any variables or objects that existed before it was called (no mutation of external state), and given the same inputs it always returns the same output. React expects your components to follow the same rules during the render phase — the part of a component’s life where React calls your function to figure out what JSX to produce.
Concretely, during render a component must not:
- Mutate props, state, or any object/array it did not create during this render call.
- Mutate a variable or object declared outside the component (a module-level array, a ref’s
.current, a global counter). - Produce different output for the same props and state (for example by reading
Math.random()orDate.now()directly in the JSX). - Perform side effects such as network requests, subscriptions, timers, or direct DOM manipulation.
Why does React insist on this? Because React does not guarantee it will call your component function exactly once per update. In development, <StrictMode> deliberately calls each component function twice in a row (throwing away one result) specifically to help you notice impure renders — if your component mutates something on the first call, the second call sees corrupted data and behaves differently, and you’ll notice a bug that would otherwise stay hidden until production. In concurrent rendering, React may also start rendering a tree, pause it, throw the partial work away, and start over later (for example when a more urgent update comes in) — that only works safely if calling your function twice, or abandoning a call halfway through, has no observable side effects.
Side effects themselves are not banned from components — they’re just not allowed to live inside the render phase. React gives you useEffect (and event handlers) as the place to put them, because effects run after React has already committed the render to the DOM, outside of the render calculation itself.
Syntax
There isn’t special syntax for purity — it’s a discipline you apply to a normal function component. The shape to aim for looks like this:
function MyComponent(props) {
// Calculate JSX from props/state only. No mutation. No side effects.
const result = someCalculation(props.value);
return <div>{result}</div>;
}
| Rule | Allowed during render | Where it belongs instead |
|---|---|---|
| Read props/state | Yes | — |
| Create new local variables/objects | Yes | — |
| Mutate props, state, or outside variables | No | Event handler (via a setter function) |
| Network requests, subscriptions, timers | No | useEffect |
Reading Math.random(), Date.now() |
No (non-deterministic) | Compute once with useState/useMemo, or in an effect/handler |
| Direct DOM manipulation | No | useEffect with a ref |
Examples
Example 1: A pure component
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
function App() {
return (
<>
<Greeting name="Ada" />
<Greeting name="Grace" />
</>
);
}
Renders two headings: “Hello, Ada!” and “Hello, Grace!”. Greeting only reads its name prop and returns JSX built from it — nothing outside the function is touched, and calling it again with the same prop always produces the same result. This is exactly the shape React expects.
Example 2: Breaking purity by mutating a shared variable
let guestCount = 0;
function Guest() {
guestCount = guestCount + 1; // mutates a variable outside the component
return <li>Guest #{guestCount}</li>;
}
function GuestList() {
return (
<ul>
<Guest />
<Guest />
<Guest />
</ul>
);
}
This looks like it should render “Guest #1”, “Guest #2”, “Guest #3”, and in a single normal render it might. But guestCount lives outside every component instance, so the numbers depend on how many times React happens to call Guest — under <StrictMode>‘s double-invoke in development, or if React re-renders GuestList for an unrelated reason, the count keeps climbing and the numbers drift or duplicate. The output is no longer a pure function of props.
Example 3: The fixed, pure version
function Guest({ index }) {
return <li>Guest #{index}</li>;
}
function GuestList() {
const guests = [1, 2, 3];
return (
<ul>
{guests.map((index) => (
<Guest key={index} index={index} />
))}
</ul>
);
}
Renders the same “Guest #1”, “Guest #2”, “Guest #3” list, but now every number comes from a prop computed inside GuestList‘s own render, from local data (guests), not a shared mutable variable. Calling Guest any number of times with index={2} always renders “Guest #2” — it’s deterministic and safe to call twice, ten times, or not at all.
How it works step by step
- Render phase: React calls your component function to get JSX. This must be pure — no mutation, no side effects, deterministic output for given props/state. React may call this function more than once per commit (StrictMode in development, or concurrent rendering discarding and retrying work).
- Commit phase: React takes the JSX from the render phase and applies the necessary changes to the real DOM. This happens exactly once per update, after render has finished.
- Effect phase: After the commit is painted, React runs your
useEffectcallbacks. This is where side effects belong, because by now render has already produced its (single, agreed-upon) result and effects can’t corrupt it.
Because render can run more than once per update, anything impure inside it (a mutation, a random number, a fetch) can happen more than once too — which is precisely how impure components produce inconsistent UI, duplicate network requests, or numbers that only look right sometimes.
Common Mistakes
Mistake 1: Mutating state or props directly during render
function Cart({ items }) {
items.push({ name: "New Item" }); // mutates the props array directly
return <p>{items.length} items</p>;
}
This mutates the items array that belongs to the parent, so the parent’s own state changes without ever calling a setter — React never finds out, and the UI can desync from the actual data. Add new items through the parent’s state setter instead, and treat items as read-only inside Cart:
function Cart({ items, onAdd }) {
return (
<>
<p>{items.length} items</p>
<button onClick={() => onAdd({ name: "New Item" })}>Add</button>
</>
);
}
// In the parent:
// setItems((prev) => [...prev, newItem]);
Mistake 2: Non-deterministic values computed in render
function Dice() {
const roll = Math.floor(Math.random() * 6) + 1; // different every render
return <p>You rolled: {roll}</p>;
}
Every time React calls Dice — including StrictMode’s extra dev-mode call — it gets a different number, so the same render can silently show two different results depending on how many times React happened to invoke the function. Compute the value once and store it, so render only ever reads it:
import { useState } from "react";
function Dice() {
const [roll] = useState(() => Math.floor(Math.random() * 6) + 1);
return <p>You rolled: {roll}</p>;
}
Best Practices
- Treat props and state as read-only snapshots; always update them through their setter function, never by mutating them in place.
- Keep all side effects (network calls, subscriptions, timers, manual DOM edits, logging to an external service) inside
useEffector event handlers, never directly in the component body. - Avoid reading global mutable variables during render; if a component needs shared data, pass it in as props or read it from context.
- Never call
Math.random(),Date.now(), or other non-deterministic APIs directly in JSX — compute the value once withuseState‘s lazy initializer oruseMemo, or generate it outside render entirely. - Leave
<StrictMode>enabled during development specifically because it double-invokes render to surface these bugs early. - When in doubt, ask: “if React called this function twice in a row and threw one result away, would anything break?” If yes, the component isn’t pure yet.
Practice Exercises
- Take the impure
Guest/guestCountexample from this lesson and rewrite it so the numbering comes from an array index instead of a shared mutable variable, without changing the rendered output. - Write a
Timestampcomponent that is supposed to show the time it was rendered. Explain in a sentence why callingnew Date()directly in the render body is impure, and describe how you would restructure it (hint: consider whether it should be a one-time value via state, or a value that updates via an effect and a timer). - Find one place in a personal or sample project where a component mutates an object or array it received as a prop, and rewrite it to create a new object/array instead (using spread syntax) before passing it back up via a setter.
Summary
- A pure component returns the same JSX for the same props and state, and does not mutate anything outside itself while rendering.
- React relies on purity because it may call your component function more than once per update — notably,
<StrictMode>double-invokes components in development to help you catch impure code. - Mutating props, state, or outside variables during render is the most common way purity breaks.
- Non-deterministic values (
Math.random(),Date.now()) read directly in render also break purity, because they produce a different result on each call. - Side effects like fetching data, subscribing, or touching the DOM belong in
useEffector event handlers, which run outside the render calculation.
