React Components Must Be Pure: The Idea Behind StrictMode and the Compiler
Why React expects pure components, how StrictMode's double render exposes impurity, where side effects belong, and why the React Compiler relies on purity.
This article is published in English.
Sources: React Components Must Be Pure — the Secret Behind StrictMode and the Compiler
Until now the React story has been "write it this way and the screen appears". There is one rule, though, that React quietly expects every component to follow, and it explains two things that confuse most newcomers: why StrictMode calls your component twice, and how the React Compiler can optimise code so boldly. The rule is purity.
What a pure function is
Remember the maths: y = f(x) = 2x returns 6 for an input of 3. It does so yesterday, today and on the hundredth call, and it never reaches out to change anything in the world beyond itself.
A function like that is pure, which means two conditions hold.
- The same input always gives the same output.
- It has no side effects, so nothing outside its own scope is altered.
Code makes it plainer. double below is pure, while addToTotal is not.
// ✅ pure function — same input, same output, and it touches nothing outside
function double(x: number): number {
return x * 2;
}
double(3); // 6
double(3); // 6 — six no matter how many times you call it
// 🔴 impure function — it changes the outer variable `total` (a side effect)
let total = 0;
function addToTotal(x: number): number {
total += x; // side effect!
return total; // the result changes on every call
}
addToTotal(3); // 3
addToTotal(3); // 6 — same input, different result
Components are meant to be pure too
React assumes every component is a pure function. With the same props and state, it has to return the same JSX every time.
type Props = { name: string };
// ✅ pure — same name, always the same result
function Greeting({ name }: Props) {
return <h1>Hello, {name}!</h1>;
}
Hand this component the name "Jane" and you get <h1>Hello, Jane!</h1>, always. React relies on that predictability to update the screen efficiently.
Breaking the rule: touching the outside during render
Trouble begins when a component changes something outside itself while it renders.
let count = 0; // a variable outside the component
// 🔴 not pure — changes an external variable while rendering
function Counter() {
count = count + 1; // side effect!
return <p>{count}</p>;
}
Counter bumps the outer variable count on each render, so its output depends on how often it's been called: 1 the first time, 2 the second. Both purity conditions fail. React may render a component several times or in a different order, so a component like this produces bugs that are hard to predict.
Try it and see why you get 2
Run that Counter and the first thing on screen is 2, not 1, and it doesn't climb afterwards. Puzzling at first, but understanding it makes a fine lesson in purity.
Why 2? In development, StrictMode calls the component function twice when mounting, so one mount runs Counter two times.
- First call:
countgoes from 0 to 1 and the result is<p>1</p>. - Second call:
countgoes from 1 to 2 and the result is<p>2</p>.
The screen shows the second result. With StrictMode switched off, the function runs once and you'd see 1.
Why does it stop at 2 instead of going on to 3, 4 and so on? A component only runs when React re-renders it. This one has no state and its parent isn't re-rendering, so it runs at the initial mount (twice, thanks to StrictMode) and then rests, leaving count stuck.
To watch the value drift, force a re-render. The next example does that with a button click. (useState gets proper treatment later, so for now just read it as a gadget that redraws the screen when you press the button.)
import { useState } from 'react';
let count = 0;
function Counter() {
const [, setTick] = useState(0); // a device to trigger re-renders
count = count + 1; // 🔴 changes an external variable on every render
return (
<div>
<p>{count}</p>
<button onClick={() => setTick((n) => n + 1)}>Re-render</button>
</div>
);
}
Each press re-renders Counter through its own state change, and StrictMode calls it twice every time, so the number jumps by two: 2, 4, 6 and so on. Without StrictMode it would advance one at a time.
One caution about this demo. Why is the state placed inside Counter at all? If you moved it up to the parent and left Counter as a child without props, pressing the button wouldn't change the displayed 2. A component that takes no input is one whose result can never differ, so the React Compiler caches it and skips the re-render. To see the drift, you must touch the external variable inside the component that actually re-renders. Oddly, that behaviour is itself a nice illustration of the fact that the compiler assumes purity. How the compiler caches components deserves its own discussion.
Here is the core point. A pure component should give the same result however many times it's called, but this one depends on the call count. StrictMode calls it twice on purpose to expose that impurity in front of you.
The remedy is to leave the outside alone. While rendering, compute the result from your own props and state only.
// ✅ pure — computed from input (props) alone
function Counter({ count }: { count: number }) {
return <p>{count}</p>;
}
You may change what you made during the render
Purity doesn't mean changing nothing. It means not touching things that existed before the render began, such as external variables, props and state. Variables or arrays you create fresh during rendering are yours to modify. That's just computation, not a side effect.
function ProductList({ products }: { products: Product[] }) {
// ✅ this array was just created in this render — handle it however you like
const sorted = [...products].sort((a, b) => a.price - b.price);
return (
<ul>
{sorted.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
What matters is the copy made with [...products]. Sorting the original products array, which arrives as props, would modify props and break the rule. The copy was created in this render, so it's fine.
First mystery: why StrictMode runs things twice
In development, StrictMode calls component functions twice deliberately, to catch components that aren't pure. It's the same <StrictMode> wrapper placed around the app when the project was set up.
// main.tsx — in development, the components inside this render twice
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
A pure component gives identical results on both runs, so nothing happens. A component like the Counter above, which touches an external variable, shows warning signs straight away: the output drifts or a value doubles. StrictMode brings these symptoms to the surface while you're still developing, well before they harden into mysteries that are painful to track down once the app is live.
So the double execution isn't a bug. It's a health check. If running twice causes no trouble, the component is pure. In a production build it runs only once.
Where side effects do belong
You'll ask a fair question: logging, sending data to a server and starting a timer are clearly needed, so where do they go? Anywhere other than the render itself.
- Reacting to user actions such as clicks and typing belongs in event handlers.
- Syncing with an external system after the screen has been drawn belongs in Effects.
Say you log a page view straight in the render body: it will fire with every render, doubled under StrictMode while developing. Relocate that call to an event handler and it happens just when somebody actually clicks.
// 🔴 side effect during rendering — fires on every render
function ProductPage({ id }: { id: number }) {
logView(id); // a side effect in the render body — not allowed
return <h1>Product {id}</h1>;
}
// ✅ in an event handler — runs only at the moment the user clicks
function BuyButton({ id }: { id: number }) {
return <button onClick={() => logPurchase(id)}>Buy</button>;
}
Keep the render body pure and push side effects out to their proper places. That principle keeps coming back.
Second mystery: why the compiler depends on purity
If you've turned on the React Compiler, you may recall that it takes over the hand-written useMemo and useCallback. Purity is what makes that possible.
The compiler works on the assumption that components are pure. Since the same input is guaranteed to produce the same output, it considers it safe to reuse the previous result when inputs haven't changed, and inserts memoisation automatically. If a component changed the outside during rendering, reuse could produce wrong results, so the compiler skips optimisation for components that break the rule. The ESLint rule enabled alongside the compiler tells you which component is the culprit.
// 🔴 not pure → the compiler skips optimization (ESLint points at this component)
let renderCount = 0;
function RenderCounter() {
renderCount++; // mutating an external variable during render — a violation
return <p>{renderCount}</p>;
}
// ✅ pure → the compiler memoizes automatically (reuse the previous result when inputs match)
function Label({ text }: { text: string }) {
return <p>{text}</p>;
}
function App() {
return (
<>
<RenderCounter />
<Label text="Pure Component" />
</>
);
}
export default App;
A component written purely appears in DevTools with the "Memo" badge, even though no useMemo was typed.
In short, purity is the precondition for the compiler to help you. Write pure components and the performance gains follow automatically. The only thing to worry about is writing pure code.
Seeing the UI as a tree
One more perspective rounds this off. Components contain other components: App contains ProductList, which contains several ProductCards. That nesting naturally forms a tree.
function App() {
return <ProductList products={products} />;
}
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</ul>
);
}
As the components nest, this code yields the following tree.
App
└─ ProductList
├─ ProductCard
├─ ProductCard
└─ ProductCard
The tree view matters more as you go on. Data in the form of props flows down it from top to bottom, and for the state you'll learn later, deciding where in the tree to place it becomes the heart of your design. Hold the picture "screen equals component tree" in mind and even complex UI can be reasoned about structurally.
Recap
- React treats every component as a pure function: same props and state, same JSX.
- During rendering, don't change anything external, including variables, props and state. Values newly created in the current render are fine to change, for example when sorting a copy.
- StrictMode's double run in development is a health check that exposes impure components. A pure one passes untouched.
- Put side effects in event handlers or Effects, never in the render body.
- Purity is what lets the React Compiler optimise automatically, so writing pure code earns you performance.
- The UI is a component tree, and data flows down it.
So far the screen is a frozen picture: you can assemble it from components, write markup with JSX, pass data down with props and build dynamic views with conditions and loops, but pressing a button does nothing. The next step is making the screen respond to the user, and it starts with handling events.
The official React documentation pages on keeping components pure and on understanding your UI as a tree cover the same ground in more depth.