Pure Component
Problem
Your component renders one thing on Monday and something different on Tuesday, with the exact same props—maybe it reads the current time during render, accesses global state, or fires an analytics event, and you can’t predict what it’ll do next.
This unpredictability breaks everything downstream: you can’t memoize it, tests pass sometimes and fail others, and the framework can’t skip re-renders because it doesn’t know whether the output will change.
I’ve debugged components that triggered three API calls instead of one because fetching happened during render, and I’ve seen race conditions where stale data overwrote fresh data because a side effect captured an old closure value.
Solution
Make your components pure functions that produce the same output for the same inputs, every time—no reading global state during render, no checking the current time, no random values, and no side effects.
Need to fetch data? Use useEffect rather than doing it during render. Need the current time? Receive it as a prop. Need analytics? Handle it in an event handler or effect.
The rule is simple: rendering describes what the UI looks like while effects handle communication with the outside world, and mixing these concerns is where bugs come from.
Once your components are pure, memoization actually works—wrap them in React.memo and the framework skips re-renders when props haven’t changed, though this only works if same props genuinely means same output.
Example
Here’s what purity looks like in practice, and what happens when you violate it.
Basic Pure Component
// React.memo skips re-renders when props haven't changed
const Greeting = React.memo(function Greeting({ name, title }) {
return (
<div>
<h1>Hello, {name}!</h1>
{title && <p>{title}</p>}
</div>
);
}); <template>
<div>
<h1>Hello, {{ name }}!</h1>
<p>{{ greeting }}</p>
</div>
</template>
<script>
export default {
props: ['name'],
computed: {
// Computed properties are pure and automatically memoized
greeting() {
return `Welcome, ${this.name}!`;
}
}
};
</script> <script>
export let firstName;
export let lastName;
// Reactive statements are pure and memoized
$: fullName = `${firstName} ${lastName}`;
</script>
<div>
<h1>{fullName}</h1>
</div> class PureGreeting extends HTMLElement {
static get observedAttributes() { return ['name']; }
attributeChangedCallback(name, oldValue, newValue) {
if (newValue !== oldValue) this.render();
}
render() {
this.innerHTML = `<h1>Hello, ${this.getAttribute('name')}!</h1>`;
}
}
customElements.define('pure-greeting', PureGreeting); Pure vs Impure
The difference between pure and impure comes down to whether output depends solely on inputs:
function ImpureTimestamp({ prefix }) {
const time = new Date().toLocaleTimeString();
return <div>{prefix}: {time}</div>;
} function PureTimestamp({ prefix, time }) {
return <div>{prefix}: {time}</div>;
} Immutable Updates
Memoized components only detect changes through reference comparison, so mutations won’t trigger re-renders:
const UserDisplay = React.memo(({ user }) => <div>{user.name}</div>);
user.name = 'Bob';
<UserDisplay user={user} /> const updated = { ...user, name: 'Bob' };
<UserDisplay user={updated} /> Testing
Pure components are straightforward to test since you can verify that same props produce same output:
test('memoized component skips re-render with same props', () => {
const renderSpy = jest.fn();
const MemoizedGreeting = React.memo(({ name }) => {
renderSpy();
return <div>{name}</div>;
});
const { rerender } = render(<MemoizedGreeting name="Alice" />);
expect(renderSpy).toHaveBeenCalledTimes(1);
rerender(<MemoizedGreeting name="Alice" />);
expect(renderSpy).toHaveBeenCalledTimes(1); // Skipped
rerender(<MemoizedGreeting name="Bob" />);
expect(renderSpy).toHaveBeenCalledTimes(2); // Rendered
});
Side Effects in Effects, Not Render
Fetching during render causes duplicate requests and race conditions; effects provide explicit control:
function BadProfile({ userId }) {
const user = fetchUserSync(userId);
return <div>{user.name}</div>;
} function GoodProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
} Stable Callback References
Inline functions create new references on each render, breaking memoization for children:
function TodoList({ todos }) {
// useCallback keeps the same reference across renders
const handleToggle = useCallback((id) => toggleTodo(id), []);
return todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
));
}
// Only re-renders when todo or onToggle reference changes
const TodoItem = React.memo(({ todo, onToggle }) => (
<li onClick={() => onToggle(todo.id)}>{todo.text}</li>
));
Benefits
- Predictable behavior means same props always produce same output, letting you reason about components in isolation without tracing through global state.
- Memoization actually works because React.memo can confidently skip re-renders when it knows the props haven’t changed.
- Testing becomes straightforward since you just pass props and check output without needing to mock global state or worry about timing.
- Reusability improves because pure components work anywhere—they don’t secretly depend on external context that might not exist in a new location.
- Timing bugs disappear when side effects live in effects rather than render, giving you explicit control over when they run.
- Concurrent rendering becomes safe since React can render your component multiple times without triggering duplicate side effects.
Tradeoffs
- Side effects must move out of render, which means fetching, subscriptions, and analytics all go in useEffect or event handlers rather than happening inline.
- Memoization has overhead from prop comparison, and for components with frequently-changing props this cost can exceed the benefit of skipping renders.
- Shallow comparison has limits, so changing
user.namewithout changing theuserreference won’t trigger a re-render even though the data changed. - Callbacks need useCallback because inline functions create new references on every render, which breaks memoization for child components.
- Some things can’t be pure since certain integrations with imperative APIs genuinely need to happen during the render phase.
- Profile before optimizing because wrapping everything in React.memo “just in case” adds overhead without benefit if those components weren’t causing problems.
Summary
Pure components produce the same output for the same inputs with no side effects during render, which enables framework optimizations like memoization, makes testing straightforward, and eliminates timing-related bugs. Keep side effects in useEffect or event handlers, and your render function becomes a simple transformation from props to UI.