Pure Component
Problem
Your components re-render unpredictably even when given the same properties, creating a cascade of performance and reliability issues that undermine your application’s stability. You watch your performance profiler light up with hundreds of wasted re-renders, where components expend precious CPU cycles regenerating identical output when nothing meaningful has changed, while the unpredictability makes optimization impossible—you can’t safely skip re-renders when you have no guarantee that a component will produce the same output twice.
Testing becomes a nightmare as components betray the fundamental contract of determinism. You write a test that passes on the first run, then mysteriously fails on the second because the component triggered an unexpected API call during rendering, or fired off analytics events, or mutated the DOM in ways you didn’t anticipate, making your test suite unreliable and your debugging sessions frustrating as components somehow produce different outputs or side effects given identical inputs repeatedly.
The inconsistency runs deeper than surface-level bugs. Components that access external state, timestamps, random numbers, or network conditions during rendering create hidden dependencies that manifest as Heisenbugs—problems that appear and disappear without obvious cause. You trace through code trying to understand why the same props sometimes produce different results, only to discover that a component is reading from global variables, checking the current time, or making synchronous API calls during its render phase. These components can’t be safely memoized because their output depends on more than their inputs, eliminating entire categories of optimization techniques.
Race conditions emerge when side effects execute during render methods, firing multiple times or out of order as React re-renders your component tree. A component might trigger three API calls when it should have triggered one, or execute analytics tracking in the wrong sequence, or mutate shared state that other components depend on. Stale closures compound the problem as side effects capture old values of props or state, leading to updates being applied to the wrong data or UI showing information from previous renders.
Solution
Build your components around the principle of purity, where render methods function as deterministic transformations from props to JSX with no side effects. When given props A, your component should always return JSX B, with absolutely no variation based on the current time, random values, or external mutable state. This determinism transforms components from unpredictable black boxes into reliable, reasoning-friendly building blocks that behave consistently across renders.
The key insight is separating the concerns of rendering from the concerns of side effects. Your render method should be a pure function that takes data and returns UI, nothing more. All side effects—data fetching, subscriptions, analytics tracking, DOM manipulation, or state mutations—must be moved into dedicated lifecycle methods like useEffect, componentDidMount, or event handlers. This separation creates a clear boundary: rendering describes what the UI should look like, while lifecycle methods handle how to synchronize with external systems.
Within your render methods, treat props and state as immutable. Never mutate a prop object, never modify state directly, and avoid changing external variables during rendering. When you need to transform data, create new objects rather than modifying existing ones. This immutability enables reference equality checks that power performance optimizations, allowing React to quickly determine whether props have changed by comparing references rather than deep-comparing values.
Leverage your framework’s memoization features to automatically skip re-renders when props haven’t changed. In React, wrap functional components with React.memo or extend PureComponent for class components to implement shallow prop comparison. These mechanisms check whether props are referentially equal to their previous values, short-circuiting the render when nothing has changed. For Vue, computed properties and watchers provide automatic memoization. In Svelte, reactive statements only re-run when their dependencies change.
The combination of pure rendering and immutable updates creates a virtuous cycle. Because your components always produce the same output for the same inputs, the framework can safely cache and reuse previous renders. Because you update data immutably, reference checks reliably detect changes. Because side effects are isolated to lifecycle methods, renders can execute multiple times without unintended consequences. This makes your components predictable, testable, and eligible for the full spectrum of performance optimizations through intelligent memoization.
Example
This example demonstrates a pure component that produces consistent output for the same inputs and only re-renders when props actually change.
React.memo for Functional Components
// React.memo makes a component skip re-renders with same props
const Greeting = React.memo(function Greeting({ name, title }) {
// Same input always produces same output
// No side effects in render
return (
<div>
<h1>Hello, {name}!</h1>
{title && <p>{title}</p>}
</div>
);
});
// Component only re-renders when props actually change
<Greeting name="Alice" title="Engineer" />
React.memo with Custom Comparison
const UserCard = React.memo(
function UserCard({ user, onSelect }) {
return (
<div onClick={() => onSelect(user.id)}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
},
// Custom comparison function
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email
// Intentionally skip onSelect comparison if it's always new
);
}
);
React PureComponent Class
class UserList extends React.PureComponent {
// PureComponent implements shouldComponentUpdate with shallow comparison
render() {
const { users, onUserClick } = this.props;
// Pure rendering - same props always produce same output
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
}
// Component skips re-render if props are shallowly equal
Pure vs Impure Component
// ❌ IMPURE - produces different output with same props
function ImpureTimestamp({ prefix }) {
// Accesses external state (current time)
const time = new Date().toLocaleTimeString();
return <div>{prefix}: {time}</div>;
}
// ✅ PURE - same props always produce same output
function PureTimestamp({ prefix, time }) {
// Time passed as prop, no external state access
return <div>{prefix}: {time}</div>;
}
// ❌ IMPURE - side effect in render
function ImpureCounter({ count }) {
// Mutates external state during render
window.renderCount = (window.renderCount || 0) + 1;
return <div>Count: {count}</div>;
}
// ✅ PURE - side effect moved to useEffect
function PureCounter({ count }) {
useEffect(() => {
// Side effect in effect, not render
window.renderCount = (window.renderCount || 0) + 1;
});
return <div>Count: {count}</div>;
}
Vue Computed Properties (Pure)
<template>
<div>
<h1>Hello, {{ name }}!</h1>
<!-- Computed properties are pure and cached -->
<p>{{ greeting }}</p>
<p>{{ uppercaseName }}</p>
</div>
</template>
<script>
export default {
props: ['name'],
computed: {
// Pure computed property - same input always produces same output
greeting() {
return `Welcome, ${this.name}!`;
},
// Computed properties are automatically memoized
uppercaseName() {
console.log('Computing uppercase...'); // Only logs when name changes
return this.name.toUpperCase();
}
}
};
</script>
Svelte Reactive Statements (Pure)
<script>
export let firstName;
export let lastName;
// Reactive statement - pure and automatically memoized
$: fullName = `${firstName} ${lastName}`;
// Complex reactive computation - only runs when dependencies change
$: initials = firstName.charAt(0) + lastName.charAt(0);
// Reactive block with side effect (moved outside render)
$: {
console.log('Name changed:', fullName);
}
</script>
<!-- Pure rendering - same props always produce same output -->
<div>
<h1>{fullName}</h1>
<p>Initials: {initials}</p>
</div>
Immutable Updates for Pure Components
// ❌ WRONG - Mutating object breaks purity
function updateUser(user) {
user.name = 'New Name'; // Mutates original
return user;
}
// ✅ CORRECT - Immutable update preserves purity
function updateUser(user) {
return { ...user, name: 'New Name' }; // New object
}
// Pure component example
const UserDisplay = React.memo(({ user }) => {
return <div>{user.name}</div>;
});
// With mutation, component won't re-render (same reference)
const user = { name: 'Alice' };
user.name = 'Bob'; // Mutated, but reference unchanged
<UserDisplay user={user} /> // Won't update!
// With immutable update, component re-renders (new reference)
const user = { name: 'Alice' };
const newUser = { ...user, name: 'Bob' }; // New object
<UserDisplay user={newUser} /> // Updates correctly!
Web Component with Pure Rendering
class PureGreeting extends HTMLElement {
constructor() {
super();
this._name = '';
}
static get observedAttributes() {
return ['name'];
}
attributeChangedCallback(name, oldValue, newValue) {
// Only re-render if value actually changed
if (name === 'name' && newValue !== oldValue) {
this._name = newValue;
this.render();
}
}
render() {
// Pure rendering - same input always produces same output
// No side effects (API calls, analytics, etc.)
this.innerHTML = `<h1>Hello, ${this._name}!</h1>`;
}
}
customElements.define('pure-greeting', PureGreeting);
Testing Pure Components
// Pure components are easy to test
test('renders greeting with name', () => {
const { getByText } = render(<Greeting name="Alice" />);
expect(getByText('Hello, Alice!')).toBeInTheDocument();
});
test('renders same output for same input', () => {
const { rerender, getByText } = render(<Greeting name="Alice" />);
const firstRender = getByText('Hello, Alice!');
// Re-render with same props
rerender(<Greeting name="Alice" />);
const secondRender = getByText('Hello, Alice!');
// Output should be identical (pure function behavior)
expect(firstRender).toEqual(secondRender);
});
test('skips re-render with same props', () => {
const renderSpy = jest.fn();
function SpiedGreeting({ name }) {
renderSpy();
return <div>{name}</div>;
}
const MemoizedGreeting = React.memo(SpiedGreeting);
const { rerender } = render(<MemoizedGreeting name="Alice" />);
expect(renderSpy).toHaveBeenCalledTimes(1);
// Re-render with same props
rerender(<MemoizedGreeting name="Alice" />);
expect(renderSpy).toHaveBeenCalledTimes(1); // Still 1 - skipped!
// Re-render with different props
rerender(<MemoizedGreeting name="Bob" />);
expect(renderSpy).toHaveBeenCalledTimes(2); // Now 2 - rendered!
});
Separating Side Effects from Rendering
// ❌ IMPURE - side effect in render
function BadUserProfile({ userId }) {
// API call during render - causes issues
const user = fetchUserSync(userId);
return <div>{user.name}</div>;
}
// ✅ PURE - side effect in useEffect
function GoodUserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Side effect isolated to effect
fetchUser(userId).then(setUser);
}, [userId]);
// Pure rendering
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
Stable References for Pure Components
function TodoList({ todos }) {
// ❌ WRONG - Creates new function on every render
const handleToggle = (id) => {
toggleTodo(id);
};
// ✅ CORRECT - Stable function reference
const handleToggle = useCallback((id) => {
toggleTodo(id);
}, []);
return (
<ul>
{todos.map(todo => (
// Pure TodoItem only re-renders when todo changes
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</ul>
);
}
const TodoItem = React.memo(({ todo, onToggle }) => {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
);
});
Benefits
- Makes components predictable and easier to reason about since the same inputs always produce the same outputs.
- Enables performance optimizations by skipping unnecessary re-renders when props haven’t changed through shallow comparison.
- Simplifies testing since output is deterministic based on inputs - no mocking required for pure rendering logic.
- Makes components safer to reuse in different contexts because they don’t depend on external mutable state or produce side effects.
- Reduces bugs from stale closures, race conditions, or timing issues that occur with side effects in render methods.
- Facilitates concurrent rendering in React because pure components can be safely rendered multiple times or out of order.
- Improves code maintainability by separating rendering logic from side effects, creating clear boundaries of responsibility.
Tradeoffs
- Cannot perform side effects like API calls, subscriptions, or analytics directly in render method, requiring separation into lifecycle hooks.
- Requires separating side effects from rendering logic, adding complexity with useEffect, componentDidMount, and other lifecycle methods.
- Memoization adds comparison overhead that may not always be beneficial - shallow prop comparison has cost, and for frequently-changing props the overhead exceeds benefit.
- May be over-optimization for components that rarely re-render or have cheap render functions where comparison cost exceeds render cost.
- Shallow comparison can miss nested object changes - if
user.namechanges butuserreference doesn’t, pure component won’t re-render. - Custom comparison functions in React.memo add complexity and can be buggy if implemented incorrectly, causing either too many or too few re-renders.
- Requires immutable data updates for reference equality checks to work - mutating objects breaks pure component optimization by preserving references.
- Function props that create new references on every parent render cause pure components to re-render despite same data, requiring useCallback wrapper.
- Pure components don’t help if parent components re-render frequently - pure child still executes comparison logic on every parent render.
- Can create false sense of optimization - wrapping everything in React.memo without profiling may add overhead without measurable benefit.
- Debugging pure components requires understanding shallow comparison behavior, which can be counterintuitive when props appear unchanged but references differ.
- Some side effects are necessary during render for integration with imperative APIs (refs, focus management) creating tension with purity principles.
- Testing pure components still requires testing with various prop combinations to verify render logic, though side effects are separately testable.
- PureComponent in React only does shallow comparison, so deeply nested state requires additional memoization or immutability libraries like Immer.