How to fix PureComponent shallow comparison not working correctly in React
React PureComponent shallow comparison fails when props or state contain objects and arrays that are mutated in place instead of recreated with new references. PureComponent uses shallow equality checks, comparing primitives by value but objects and arrays by reference only. When complex data structures are mutated without creating new references, PureComponent won't detect changes and components won't re-render. The fix involves creating new object/array instances, avoiding inline function creation, and understanding when to use memo or custom shouldComponentUpdate instead.
Quick fix
Fixes PureComponent shallow comparison not working correctly
// Shallow comparison rules
'hello' === 'hello' // true (primitive by value)
5 === 5 // true (primitive by value)
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
arr1 === arr2 // false (different references)
const arr3 = arr1;
arr1 === arr3 // true (same reference)
// This is what PureComponent does:
const obj1 = { name: 'John' };
const obj2 = { name: 'John' };
obj1 === obj2 // false
// Mutating doesn't change reference:
obj1.name = 'Jane';
obj1 === obj1 // true (reference unchanged)- Understand what shallow comparison does
- Fix array mutations - use immutable patterns
- Fix object mutations - use object spreading
- Fix inline function creation in props
// Shallow comparison rules'hello' === 'hello' // true (primitive by value)5 === 5 // true (primitive by value)const arr1 = [1, 2, 3];const arr2 = [1, 2, 3];arr1 === arr2 // false (different references)const arr3 = arr1;arr1 === arr3 // true (same reference)// This is what PureComponent does:const obj1 = { name: 'John' };const obj2 = { name: 'John' };obj1 === obj2 // false// Mutating doesn't change reference:obj1.name = 'Jane';obj1 === obj1 // true (reference unchanged)PureComponent shallow comparison not working correctly
What this error means
React's PureComponent is a performance optimization that automatically implements shouldComponentUpdate with shallow equality comparison. However, developers often encounter situations where changes don't trigger re-renders, leading to stale UI. Shallow comparison compares primitive types (numbers, strings, booleans) by their actual value, but compares objects and arrays by reference only - whether both point to the same memory address. This means: - If you mutate an array and pass it to a PureComponent, the reference stays the same, so React thinks nothing changed - If you mutate an object property and pass it to a PureComponent, the object reference stays the same, so React skips the re-render - Nested object changes are never detected because only the top-level references are compared This is by design - shallow comparison is fast and prevents unnecessary renders. However, it creates a common pitfall: developers accustomed to deep equality checking end up confused when their UI doesn't update.
Typical symptoms
- PureComponent doesn't re-render even though props or state has changed
- Child PureComponent doesn't update when parent passes a modified object or array
- UI shows stale data that was modified but component didn't re-render
- Mutations to nested objects within props don't trigger updates
- Array mutations (push, pop, splice) don't cause the component to re-render
- Setting new values on object properties doesn't update the component
- Component works correctly as a regular Component but fails as PureComponent
- Changes are visible in console.log but not in the rendered UI
- Inline object/array literals work but computed objects don't re-render when they should
Common causes
- Mutating arrays in place (using push, pop, splice, unshift) instead of creating new arrays
- Mutating object properties in place instead of creating a new object
- Passing newly created functions (especially callbacks) as props every render, causing false updates
- Using array methods that mutate like sort() or reverse() on existing arrays
- Modifying nested object properties instead of spreading/cloning objects
- Creating inline object literals in JSX that create new references every render
- Creating inline function definitions in render methods that create new references every render
- Mixing PureComponent with external state libraries that don't trigger proper updates
- Not understanding that shallow comparison doesn't work with complex nested data structures
- Relying on PureComponent for optimization without verifying data immutability practices
How to fix "PureComponent shallow comparison not working correctly"
Shallow comparison works like this:
// Shallow comparison rules
'hello' === 'hello' // true (primitive by value)
5 === 5 // true (primitive by value)
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
arr1 === arr2 // false (different references)
const arr3 = arr1;
arr1 === arr3 // true (same reference)
// This is what PureComponent does:
const obj1 = { name: 'John' };
const obj2 = { name: 'John' };
obj1 === obj2 // false
// Mutating doesn't change reference:
obj1.name = 'Jane';
obj1 === obj1 // true (reference unchanged)PureComponent compares props and state shallowly - it checks if the objects at the top level reference the same memory location. It does NOT look inside objects to compare nested values.
To verify shallow comparison is the issue:
1. Convert the component to a regular Component and verify it works correctly
2. Check if data is being mutated (modified in place) rather than replaced with new instances
Never mutate arrays in place. Instead, create new array instances:
// DON'T do this with PureComponent:
this.state.items.push(newItem);
this.setState({ items: this.state.items });
// DO this instead - create new array:
this.setState(prevState => ({
items: [...prevState.items, newItem]
}));
// Or use concat:
this.setState(prevState => ({
items: prevState.items.concat(newItem)
}));
// Remove item - don't use splice:
// WRONG:
this.state.items.splice(index, 1);
this.setState({ items: this.state.items });
// CORRECT:
this.setState(prevState => ({
items: prevState.items.filter((_, i) => i !== index)
}));
// Update item in array - don't mutate:
// WRONG:
this.state.items[0].name = 'updated';
this.setState({ items: this.state.items });
// CORRECT:
this.setState(prevState => ({
items: prevState.items.map((item, i) =>
i === 0 ? { ...item, name: 'updated' } : item
)
}));
// Reorder array - don't use sort/reverse on original:
// WRONG:
this.state.items.sort();
this.setState({ items: this.state.items });
// CORRECT:
this.setState(prevState => ({
items: [...prevState.items].sort()
}));The key pattern: Create a NEW array instance every time you need to update it.
Never mutate object properties in place. Create new objects:
// DON'T do this:
this.state.user.name = 'Jane';
this.setState({ user: this.state.user });
// DO this - create new object:
this.setState(prevState => ({
user: { ...prevState.user, name: 'Jane' }
}));
// Update multiple properties:
this.setState(prevState => ({
user: {
...prevState.user,
name: 'Jane',
email: 'jane@example.com'
}
}));
// Update nested objects - spread at each level:
// WRONG:
this.state.user.address.city = 'New York';
this.setState({ user: this.state.user });
// CORRECT:
this.setState(prevState => ({
user: {
...prevState.user,
address: {
...prevState.user.address,
city: 'New York'
}
}
}));
// Using Object.assign also creates new reference:
this.setState(prevState => ({
user: Object.assign({}, prevState.user, { name: 'Jane' })
}));Always remember: Create a NEW object with the updated values rather than modifying the existing one.
Every time you create a new function, it gets a new reference. This causes PureComponent to think props changed even when they didn't:
// WRONG - creates new function every render:
<ChildPureComponent onClick={() => this.handleClick()} />
// CORRECT - use class method:
<ChildPureComponent onClick={this.handleClick} />
// Or bind in constructor:
class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick = () => {
// ...
}
}
// With functional component parent and useCallback:
function Parent() {
const handleClick = React.useCallback(() => {
// ...
}, []);
return <ChildPureComponent onClick={handleClick} />;
}
// With functional components, use memo instead of PureComponent:
const ChildPureComponent = React.memo(function Child({ onClick }) {
return <button onClick={onClick}>Click me</button>;
});The rule: If passing a function to a PureComponent child, ensure the function reference stays the same across renders.
Creating objects or arrays inline in JSX creates new references every render:
// WRONG - new object every render:
<ChildPureComponent style={{ color: 'red' }} />
// CORRECT - define outside or in constructor:
const styles = { color: 'red' };
class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.styles = { color: 'red' };
}
render() {
return <ChildPureComponent style={this.styles} />;
}
}
// WRONG - new array every render:
<ChildPureComponent items={[1, 2, 3]} />
// CORRECT:
class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.items = [1, 2, 3];
}
render() {
return <ChildPureComponent items={this.items} />;
}
}
// With functional components, use useMemo:
function Parent() {
const styles = React.useMemo(
() => ({ color: 'red' }),
[]
);
const items = React.useMemo(
() => [1, 2, 3],
[]
);
return <ChildPureComponent style={styles} items={items} />;
}PureComponent might not be the right choice for all scenarios:
// Use PureComponent for class components with simple props/state:
class SimpleList extends React.PureComponent {
render() {
return <div>{this.props.items.join(', ')}</div>;
}
}
// Use React.memo for functional components:
const SimpleList = React.memo(function({ items }) {
return <div>{items.join(', ')}</div>;
});
// Use custom shouldComponentUpdate for complex comparison logic:
class ComplexComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Custom deep comparison or other logic
return !deepEqual(this.props, nextProps) ||
!deepEqual(this.state, nextState);
}
render() {
// ...
}
}
// Use useMemo in functional components for expensive computations:
function Component({ items }) {
const sorted = React.useMemo(
() => items.slice().sort(),
[items]
);
return <div>{sorted.join(', ')}</div>;
}
// Use useCallback to memoize callbacks:
function Parent() {
const handleClick = React.useCallback((id) => {
// ...
}, []);
return <ChildPureComponent onItemClick={handleClick} />;
}When to use each:
- PureComponent: Class components with simple props/state that follow immutability patterns
- React.memo: Functional components that don't need state
- useMemo: Expensive computations that depend on specific dependencies
- useCallback: Function references that need to stay stable across renders
- Custom shouldComponentUpdate: Complex comparison logic that shallow check can't handle
Add logging to verify shallow comparison is working correctly:
class DebugPureComponent extends React.PureComponent {
componentDidUpdate(prevProps, prevState) {
console.log('PureComponent re-rendered');
console.log('Props changed?', prevProps !== this.props);
console.log('State changed?', prevState !== this.state);
}
render() {
return <div>{this.props.data}</div>;
}
}
// In parent:
class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
user: { name: 'John' },
items: [1, 2, 3]
};
}
updateUser = () => {
// This will correctly trigger re-render
this.setState(prevState => ({
user: { ...prevState.user, name: 'Jane' }
}));
}
addItem = () => {
// This will correctly trigger re-render
this.setState(prevState => ({
items: [...prevState.items, 4]
}));
}
render() {
return (
<>
<DebugPureComponent data={this.state.user.name} />
<button onClick={this.updateUser}>Update User</button>
<button onClick={this.addItem}>Add Item</button>
</>
);
}
}Check browser DevTools console to see if the component re-renders at the expected times. If it's not re-rendering when you expect it to, review steps 2-5 to ensure you're following immutability patterns.
Advanced notes
Deep vs Shallow Comparison: Shallow comparison is much faster than deep comparison (O(n) vs O(deep recursion)), which is why React uses it by default. Deep comparison would check every nested property, making it slow for large objects. PureComponent trades deep checking for performance.
Functional Components and Hooks: Modern React development often prefers functional components with hooks (useState, useCallback, useMemo) over class-based PureComponent. React.memo with custom comparison is more flexible for functional components.
Redux and External State: If using Redux, selectors should return the same reference when the underlying data hasn't changed. Libraries like reselect use memoization to ensure shallow comparison works correctly with Redux.
Performance Profiling: Use React DevTools Profiler to identify unnecessary re-renders. Sometimes the issue isn't shallow comparison but other performance problems. Over-optimization with PureComponent can sometimes hide real issues.
Immer Library: If working with deeply nested immutable data becomes tedious, consider using Immer, which allows you to write immutable updates with mutable-looking syntax: produce(state, draft => { draft.user.name = 'Jane'; }).
TypeScript and PureComponent: TypeScript's strict mode will help catch some mutation patterns by preventing reassignment to readonly properties if you properly type your state.
Testing: When testing PureComponent, remember that shallow comparison means the same test data object won't trigger updates. Create new instances in tests: setState({ ...state, prop: newValue }) instead of mutating directly.
Common Library Issues: Some state management libraries (like older versions of Redux) don't guarantee immutability. If using such libraries, you may need custom shouldComponentUpdate or switch to libraries that enforce immutability patterns.
Frequently asked questions
- What does "PureComponent shallow comparison not working correctly" mean?
- React's PureComponent is a performance optimization that automatically implements shouldComponentUpdate with shallow equality comparison. However, developers often encounter situations where changes don't trigger re-renders, leading to stale UI. Shallow comparison compares primitive types (numbers, strings, booleans) by their actual value, but compares objects and arrays by reference only - whether both point to the same memory address.
- How do I fix "PureComponent shallow comparison not working correctly"?
- React PureComponent shallow comparison fails when props or state contain objects and arrays that are mutated in place instead of recreated with new references. PureComponent uses shallow equality checks, comparing primitives by value but objects and arrays by reference only. When complex data structures are mutated without creating new references, PureComponent won't detect changes and components won't re-render. The fix involves creating new object/array instances, avoiding inline function creation, and understanding when to use memo or custom shouldComponentUpdate instead.
- What causes "PureComponent shallow comparison not working correctly"?
- Mutating arrays in place (using push, pop, splice, unshift) instead of creating new arrays Mutating object properties in place instead of creating a new object Passing newly created functions (especially callbacks) as props every render, causing false updates
- Is the "PureComponent shallow comparison not working correctly" error serious?
- It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- 📚React Official Documentation
- 📖React: PureComponent Documentation(Official Docs)
- 📖React Legacy: Shallow Compare(Official Docs)
- 📖React: memo for Functional Components(Official Docs)
- 📖React: useCallback Hook(Official Docs)
- 📖React: useMemo Hook(Official Docs)