Skip to content
React rc components 5 min read

Components & Purity

React is built on a simple but powerful assumption: your components are pure functions of their inputs. Given the same props and state, a component must always return the same JSX, and it must not change anything outside of itself while rendering. This contract is what lets React skip re-renders, render in the background, and run your code twice in development without breaking your app. Understanding purity is the difference between predictable UIs and the mysterious “it only breaks sometimes” bugs.

What does a pure component mean?

A pure function has two properties: it is idempotent (same inputs produce the same output) and it has no side effects (it doesn’t touch anything that existed before it ran). A React component should behave the same way during rendering. The “inputs” are its props, state, and context; the “output” is the JSX it returns.

// Pure: output depends only on props
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

Render this with name="Ada" a thousand times and you always get the same markup. Nothing in the surrounding world changed. That is exactly what React wants.

What counts as a side effect during render?

A side effect is any work that reaches outside the component’s own scope while it renders: mutating a variable declared elsewhere, writing to the DOM directly, making a network request, setting timers, or logging to an external store. The render phase should be a pure calculation.

// Impure: mutates a variable from outside its scope
let cart = [];

function ProductRow({ product }) {
  cart.push(product); // side effect during render!
  return <li>{product.name}</li>;
}

Each render pushes again, so the result depends on how many times React chose to render rather than on the props. With concurrent rendering and StrictMode, that count is not under your control, so the output becomes unpredictable.

The pure version keeps the calculation local:

function ProductList({ products }) {
  // Derive, don't mutate shared state
  const rows = products.map((product) => (
    <li key={product.id}>{product.name}</li>
  ));
  return <ul>{rows}</ul>;
}

Mutating props, state, or any object created in a previous render is also impure. Always create new objects/arrays rather than editing existing ones during render.

Why React relies on purity

Purity is not stylistic advice; React’s performance features depend on it.

React featureWhat purity enables
Bailing out of re-rendersIf inputs are unchanged, React can reuse the previous output safely.
React.memo / useMemoCaching is only valid when the same inputs guarantee the same result.
Concurrent renderingReact can pause, resume, or discard a render without corrupting state.
Server renderingThe same component must produce identical HTML on the server and client.

If a component secretly depends on the outside world, any of these optimizations can produce a stale or wrong UI.

StrictMode and double-invocation

In development, wrapping your tree in <StrictMode> causes React to intentionally call your component functions twice (and run certain effect setups twice). This is a purity smoke test: a pure component produces the same result both times, so you never notice. An impure one shows doubled or corrupted behavior immediately.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>
);

With the impure cart.push example above, StrictMode reveals the bug instantly:

Output:

Render #1 -> cart length: 3
Render #2 -> cart length: 6   // doubled, because render mutated shared state

StrictMode double-invocation only happens in development and never ships to production. If something breaks only with StrictMode on, the bug is in your code, not in React.

Where side effects belong

Side effects are necessary—you do need to fetch data, set up subscriptions, and write to localStorage. They simply must not run during render. React gives you two correct homes for them.

Event handlers run in response to a user action, not during render, so they are the natural place for most effects:

function SaveButton({ draft }) {
  function handleClick() {
    localStorage.setItem("draft", JSON.stringify(draft)); // fine: not during render
  }
  return <button onClick={handleClick}>Save</button>;
}

Effects (useEffect) run after React commits the render to the screen, for synchronizing with external systems:

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let active = true;
    async function load() {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (active) setUser(data);
    }
    load();
    return () => {
      active = false; // cleanup avoids setting state after unmount
    };
  }, [userId]);

  if (!user) return <p>Loading…</p>;
  return <h2>{user.name}</h2>;
}

The render itself stays pure—it only reads user and returns JSX. The fetch lives in an effect that React controls.

Pure vs impure at a glance

PatternPure?Why
return <p>{a + b}</p>YesDerives output from inputs only
props.items.push(x) during renderNoMutates a prop
Math.random() in JSXNoDifferent output each render
Reading new Date() in renderNoOutput depends on time, not inputs
setCount(c => c + 1) inside render bodyNoTriggers state change during render
fetch() inside useEffectYesSide effect lives outside render

Best Practices

  • Treat the render phase as a pure calculation: read props, state, and context, then return JSX—nothing more.
  • Never mutate props, state, or objects from a previous render; create new values instead.
  • Keep all I/O (network, storage, DOM, timers, logging) in event handlers or useEffect, never in the render body.
  • Always enable <StrictMode> in development so impure components surface early.
  • For non-pure inputs like the current time or random values, compute them in an effect or event handler and store the result in state.
  • Use React.memo and useMemo only on genuinely pure components—caching an impure component hides bugs.
Last updated June 14, 2026
Was this helpful?