Component Purity and StrictMode in React

Component Purity and StrictMode in React

Learn why keeping React components pure is essential for predictable behavior. Discover how StrictMode helps catch side effects and follow best practices for writing clean, maintainable components.

Aurora Scharff

Aurora Scharff

July 9, 2025

Component purity ensures your components always produce the same output for the same inputs, making them predictable and easy to compose together. When combined with React's StrictMode, you can catch side effects early and follow best practices for writing clean, maintainable components.

This article explores what makes components pure, examines common violations that break purity, and demonstrates how StrictMode helps catch these issues before they reach production.

What Are Pure Components?

A pure component is like a mathematical function: it always returns the same output for the same inputs without changing anything outside itself. This predictability is what makes React applications reliable and easy to reason about.

According to React's documentation, a pure component or hook has three key characteristics:

  • Idempotent: You always get the same result every time you run it with the same inputs (props, state, context for components; arguments for hooks)
  • No side effects in render: Code with side effects should run separately from rendering. Use event handlers when users interact with the UI, or Effects that run after render.
  • Does not mutate non-local values: Components and hooks should never modify preexisting variables or objects while rendering

Let's see what this looks like in practice with a simple component:

function UserProfile({ player }) {
  return (
    <div>
      <h1>{player.username}</h1>
      <p>Score: {player.score}</p>
    </div>
  );
}

When you call this component with the same player prop:

<UserProfile player={{ username: "Alex", score: 1250 }} />

It will always return:

<div>
  <h1>Alex</h1>
  <p>Score: 1250</p>
</div>

No matter how many times React renders this component, the output remains identical. This component is pure because it only reads from its props and always produces the same JSX for the same player values. The displayed content is consistent and predictable: identical inputs always produce identical outputs.

Compare this to an impure component that modifies external state:

let profileViews = 0;

function UserProfile({ player }) {
  profileViews++;
  return (
    <div>
      <h1>{player.username}</h1>
      <p>Score: {player.score}</p>
      <p>View #{profileViews}</p>
    </div>
  );
}

When you call it with the same player prop as before:

<UserProfile player={{ username: "Alex", score: 1250 }} />

Each render produces different content, even with identical props. On the first render, you might see:

<div>
  <h1>Alex</h1>
  <p>Score: 1250</p>
  <p>View #1</p>
</div>

Then on the next render:

<div>
  <h1>Alex</h1>
  <p>Score: 1250</p>
  <p>View #2</p>
</div>

Then again:

<div>
  <h1>Alex</h1>
  <p>Score: 1250</p>
  <p>View #3</p>
</div>

The impure version creates unpredictable behavior because the same props can produce different outputs. This unpredictability makes debugging difficult and breaks React's assumptions about component behavior.

This is why pure components are so valuable. When you know a component will always behave the same way with the same inputs, you can confidently use it anywhere in your application. This predictability also enables React's optimizations like safely skipping renders when inputs haven't changed, pausing and resuming rendering for concurrent features, and producing deterministic outputs for reliable testing.

Common Purity Violations

While pure components are straightforward to write once you understand the principles, developers need to be aware of certain patterns that violate purity.

Let's examine some common patterns that violate purity and learn how to recognize them.

Modifying Props or External Variables

A common violation is modifying data that doesn't belong to the component. This often happens when developers try to "normalize" or "enhance" incoming data directly in the render function. Here's an example with our UserProfile component:

function UserProfile({ player }) {
  player.username = player.username.trim().toLowerCase();
  return (
    <div>
      <h1>{player.username}</h1>
      <p>Score: {player.score}</p>
    </div>
  );
}

This mutation happens silently on each render, modifying the original data. If this player object is used elsewhere in your app, those components will unexpectedly receive the modified data.

Instead, create local copies when you need to transform data:

function UserProfile({ player }) {
  const normalizedUsername = player.username.trim().toLowerCase();
  return (
    <div>
      <h1>{normalizedUsername}</h1>
      <p>Score: {player.score}</p>
    </div>
  );
}

This creates a new local variable without mutating the original prop. It's completely fine to modify variables and objects that you create during rendering—the issue is only with mutating existing objects from outside the component.

Breaking Idempotency

Another common violation is using values that change unpredictably, breaking the idempotent nature of components. Here's an example with our UserProfile component:

function UserProfile({ player }) {
  const lastVisited = new Date();
  return (
    <div>
      <h1>{player.username}</h1>
      <p>Score: {player.score}</p>
      <p>Last visited: {lastVisited.toLocaleString()}</p>
    </div>
  );
}

Each render produces unpredictable content, even with identical props. When the component re-renders (due to parent updates, state changes, etc.), the output changes even though the inputs haven't changed.

This doesn't mean you shouldn't use non-idempotent functions like new Date() at all – you should just avoid using them during render.

Instead, handle this with useState and its initializer function:

function UserProfile({ player }) {
  const [lastVisited] = useState(() => new Date());

  return (
    <div>
      <h1>{player.username}</h1>
      <p>Score: {player.score}</p>
      <p>Last visited: {lastVisited.toLocaleString()}</p>
    </div>
  );
}

When you do need side effects in your components, they belong in event handlers (which don't run during render) or in useEffect (which runs after render).

React StrictMode: Catching Impurity

How can components that violate purity be caught before production? React's StrictMode addresses this challenge.

StrictMode is a development tool that helps catch bugs by calling some of your functions twice. It only runs in development, so it won't affect your production performance.

To enable StrictMode, wrap your app component:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

How StrictMode Works

StrictMode helps you find bugs by running your code twice in development:

  • Components re-render an extra time to catch impure rendering bugs
  • Effects run an extra time to catch missing cleanup bugs
  • Ref callbacks run an extra time to catch missing cleanup bugs

Since pure functions should work the same way every time you call them, running them twice reveals when something is impure.

StrictMode in Action

See this in action with the same impure component from our first example.

Here it is again, with an additional console.log to track how many times it renders:

let profileViews = 0;

function UserProfile({ player }) {
  profileViews++;
  console.log(`Rendering UserProfile, views: ${profileViews}`);
  return (
    <div>
      <h1>{player.username}</h1>
      <p>Score: {player.score}</p>
      <p>View #{profileViews}</p>
    </div>
  );

In development with StrictMode, when you pass the same prop to this impure component:

<UserProfile player={{ username: "Alex", score: 1250 }} />

You would see in the console:

Rendering UserProfile, views: 1
Rendering UserProfile, views: 2

And initially see in the UI:

<div>
  <h1>Alex</h1>
  <p>Score: 1250</p>
  <p>View #2</p>
</div>

The fact that the component shows "View #2" on initial load and logs different values immediately tells you something is wrong. StrictMode's double-invocation exposes the side effect, making impurity obvious.

In production, StrictMode is disabled, so the component would only render once. You'd see one console log and "View #1" in the UI.

When you fix your component to render properly in StrictMode, you also fix many possible future production bugs.

StrictMode with Effects

StrictMode doesn't stop at components; it also tests your effects. This is crucial because effects often involve subscriptions, timers, or connections that need proper cleanup.

StrictMode tests your effects by running them in a Setup → Cleanup → Setup pattern to catch subscription leaks and cleanup issues.

Consider this game lobby component that's missing cleanup:

function GameLobby({ lobbyId }) {
  useEffect(() => {
    console.log(`Joining "${lobbyId}" lobby...`);
    const connection = createLobbyConnection(lobbyId);
    connection.join();
  }, [lobbyId]);

  return <h1>Lobby: {lobbyId}</h1>;
}

With StrictMode enabled, the developer console shows the bug:

Joining "arena-1" lobby...
Joining "arena-1" lobby...

The double joining reveals the problem! Without cleanup, each effect run creates a new connection without cleaning up the previous one. This can lead to multiple subscriptions causing users to receive duplicate messages or events, resource exhaustion where too many open connections can crash the application, or inconsistent state where multiple connections might conflict with each other.

The fix requires adding a cleanup function:

function GameLobby({ lobbyId }) {
  useEffect(() => {
    console.log(`Joining "${lobbyId}" lobby...`);
    const connection = createLobbyConnection(lobbyId);
    connection.join();
    return () => {
      console.log(`Leaving "${lobbyId}" lobby...`);
      connection.leave();
    };
  }, [lobbyId]);

  return <h1>Lobby: {lobbyId}</h1>;
}

With proper cleanup, StrictMode shows in the console that the effect can handle remounting correctly:

Joining "arena-1" lobby...
Leaving "arena-1" lobby...
Joining "arena-1" lobby...

In production, you'd only see:

Joining "arena-1" lobby...

Notice how the cleanup function runs between effect executions, properly cleaning up the connection before creating a new one. This ensures code works correctly when users navigate between pages. React can safely stop and restart effects without causing bugs.

StrictMode also helps find bugs in callback refs by calling their setup and cleanup functions an extra time to ensure they work correctly.

Conclusion

Pure components are the foundation of predictable React applications. By understanding the three key characteristics and avoiding common violations like mutating props or using non-idempotent functions in render, you can write components that behave consistently.

StrictMode helps catch these violations early through its double-invocation strategy, exposing impure behavior in development before it causes production bugs.

Together, these practices help you build React applications that are easier to debug, test, and maintain as they grow.


Sources:

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)