Pure components and Strict Mode

React · Practical · 10 min

React relies on purity to re-render, pause and reuse work safely; impure components break in subtle, intermittent ways.

React assumes every component is a pure function during rendering:

  • Same inputs, same output. Given the same props, state and context, it returns the same JSX.
  • No side effects while rendering. It doesn't change variables or objects that existed before the render, doesn't touch the DOM, doesn't start requests or timers, doesn't write to localStorage.

Creating and mutating local variables is fine; it's changing pre-existing things that's forbidden. A counter defined outside the component and incremented during render, for example, gives different results each time the component renders.

Why this matters: React may call your component more often than you expect, render it and throw the result away (concurrent rendering), render parts of the tree in a different order, or skip rendering entirely when it can reuse a previous result (the React Compiler relies on this). Pure components make all of that invisible.

Side effects still have a home:

  • Event handlers for effects caused by a user action (click, submit, type).
  • Effects (useEffect) for synchronizing with something outside React because the component is on screen.

Strict Mode (<StrictMode> wrapping your app, as Vite's template does) helps find impurity in development only. It renders each component twice, runs effects' setup, cleanup and setup again on mount, and does the same for ref callbacks. Pure code behaves identically; impure code shows doubled output or broken behaviour, pointing you at the bug. That's why you might see console logs twice in development. Production builds don't double anything.

Example

// Impure: mutates a variable that exists outside the render
let guest = 0;
function BadCup() {
  guest = guest + 1;                    // side effect during render
  return <h2>Tea cup for guest #{guest}</h2>;
}

// Pure: output depends only on props
function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  // Local mutation inside render is fine: the array is created here
  const cups = [];
  for (let i = 1; i <= 3; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return (
    <>
      {cups}
      {/* Under StrictMode this shows #2, #4, #6: the double render exposes the bug */}
      <BadCup /><BadCup /><BadCup />
    </>
  );
}

Remember

  • Rendering must be pure: same inputs, same JSX, no changes to pre-existing values
  • Put side effects in event handlers, or in effects when they sync with an external system
  • Strict Mode double-renders and re-runs effects in development to expose impure code

Watch out for

  • Removing <StrictMode> to 'fix' double logs or a double request instead of fixing the impure code or adding effect cleanup.

Check yourself

  1. Under Strict Mode in development, a component logs once per render. You see two logs on mount. Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn/keeping-components-pure