What are "pure" components (Pure Components)?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React

Brief Answer

Pure Components are React components that automatically implement shallow comparison of props and state for performance optimization. They re-render only when input data changes.

Key characteristics:

  • Automatic optimization through shallow comparison
  • Prevent unnecessary re-renders
  • Work only with immutable data
  • Available for both class and functional components

Limitations:

  • Don’t work with mutating data
  • Can produce unexpected results when used incorrectly
  • Shallow comparison isn’t suitable for complex objects

Key rule: Use pure components only with immutable data and when properly managing state.


Full Answer

Pure components are a special type of React components that automatically optimize performance by implementing shallow comparison of props and state. They re-render only when input data changes.

What are pure components

Pure components implement the shouldComponentUpdate method with shallow comparison of props and state:

// Regular component - always re-renders
class RegularComponent extends Component {
  render() {
    console.log('RegularComponent renders');
    return <div>{this.props.value}</div>;
  }
}
 
// Pure component - re-renders only when props/state change
class PureComponentExample extends PureComponent {
  render() {
    console.log('PureComponent renders');
    return <div>{this.props.value}</div>;
  }
}
 
// Usage
function App() {
  const [count, setCount] = useState(0);
  
  // This data doesn't change
  const data = { name: 'Alexander' };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increase: {count}
      </button>
      
      {/* Regular component will re-render on every App render */}
      <RegularComponent value={data.name} />
      
      {/* Pure component re-renders only if data.name changes */}
      <PureComponentExample value={data.name} />
    </div>
  );
}

How pure components work

1. Shallow comparison

Pure components use shallow comparison of props and state:

// Example of shallow comparison
function shallowEqual(objA, objB) {
  if (objA === objB) {
    return true;
  }
  
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false;
  }
  
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  
  if (keysA.length !== keysB.length) {
    return false;
  }
  
  // Compare only first-level properties
  for (let i = 0; i < keysA.length; i++) {
    if (!objB.hasOwnProperty(keysA[i]) || 
        objA[keysA[i]] !== objB[keysA[i]]) {
      return false;
    }
  }
  
  return true;
}

2. Difference between regular and pure components

// Regular component
class RegularCounter extends Component {
  render() {
    return (
      <div>
        <p>Counter: {this.props.count}</p>
        <button onClick={this.props.onIncrement}>
          Increase
        </button>
      </div>
    );
  }
}
 
// Pure component
class PureCounter extends PureComponent {
  render() {
    return (
      <div>
        <p>Counter: {this.props.count}</p>
        <button onClick={this.props.onIncrement}>
          Increase
        </button>
      </div>
    );
  }
}
 
// Demonstrating the difference
function App() {
  const [count, setCount] = useState(0);
  const [otherState, setOtherState] = useState('');
  
  // Object that doesn't change
  const config = { theme: 'light' };
  
  const handleIncrement = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <input 
        value={otherState} 
        onChange={(e) => setOtherState(e.target.value)} 
        placeholder="Other state"
      />
      
      {/* RegularCounter will re-render when otherState changes */}
      <RegularCounter count={count} onIncrement={handleIncrement} />
      
      {/* PureCounter re-renders only when count changes */}
      <PureCounter count={count} onIncrement={handleIncrement} />
    </div>
  );
}

Pure components in functional components

With hooks, functional components can use memoization:

import { memo, useState } from 'react';
 
// Regular functional component
function RegularComponent({ name, age }) {
  console.log('RegularComponent renders');
  return (
    <div>
      <h1>{name}</h1>
      <p>Age: {age}</p>
    </div>
  );
}
 
// Memoized component (PureComponent equivalent)
const MemoizedComponent = memo(function MemoizedComponent({ name, age }) {
  console.log('MemoizedComponent renders');
  return (
    <div>
      <h1>{name}</h1>
      <p>Age: {age}</p>
    </div>
  );
});
 
// With custom comparison function
const CustomMemoizedComponent = memo(
  function CustomMemoizedComponent({ user }) {
    console.log('CustomMemoizedComponent renders');
    return (
      <div>
        <h1>{user.name}</h1>
        <p>Age: {user.age}</p>
      </div>
    );
  },
  // Custom comparison function
  (prevProps, nextProps) => {
    return prevProps.user.name === nextProps.user.name && 
           prevProps.user.age === nextProps.user.age;
  }
);
 
// Usage
function App() {
  const [count, setCount] = useState(0);
  
  // This data doesn't change between renders
  const userData = { name: 'Alexander', age: 25 };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      
      <RegularComponent name={userData.name} age={userData.age} />
      <MemoizedComponent name={userData.name} age={userData.age} />
      <CustomMemoizedComponent user={userData} />
    </div>
  );
}

When to use pure components

✅ Suitable for:

// 1. Components with primitive props
function UserInfo({ name, age, email }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
    </div>
  );
}
const PureComponentUserInfo = memo(UserInfo);
 
// 2. Components with immutable objects
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <div className={todo.completed ? 'completed' : ''}>
      <span>{todo.text}</span>
      <button onClick={() => onToggle(todo.id)}>Toggle</button>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
}
const PureComponentTodoItem = memo(TodoItem);
 
// 3. Components with arrays (if array is recreated when changed)
function TodoList({ todos, onToggle, onDelete }) {
  return (
    <div>
      {todos.map(todo => (
        <PureComponentTodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </div>
  );
}

❌ Not suitable for:

// 1. Components with mutating objects
class BadExample extends PureComponent {
  render() {
    // ❌ If parentData mutates, component won't re-render
    return <div>{this.props.parentData.value}</div>;
  }
}
 
// 2. Components with functions created in parent
function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ New function on every render
  const handleClick = () => {
    console.log('Clicked!');
  };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      {/* Component will re-render due to new function */}
      <PureComponentWithFunction onClick={handleClick} />
    </div>
  );
}
 
// ✅ Correct approach - memoize function
function GoodParent() {
  const [count, setCount] = useState(0);
  
  // ✅ Function created once
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      {/* Component won't re-render */}
      <PureComponentWithFunction onClick={handleClick} />
    </div>
  );
}

Practical Examples

1. Optimizing list items

import { memo, useState, useCallback } from 'react';
 
// Pure component for list item
const ListItem = memo(function ListItem({ item, onUpdate, onDelete }) {
  console.log(`ListItem ${item.id} renders`);
  
  return (
    <div className="list-item">
      <span>{item.text}</span>
      <button onClick={() => onUpdate(item.id, `${item.text} (updated)`)}>
        Update
      </button>
      <button onClick={() => onDelete(item.id)}>
        Delete
      </button>
    </div>
  );
});
 
// List component
const List = memo(function List({ items, onUpdate, onDelete }) {
  console.log('List renders');
  
  return (
    <div>
      {items.map(item => (
        <ListItem 
          key={item.id} 
          item={item} 
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      ))}
    </div>
  );
});
 
// Parent component
function App() {
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' }
  ]);
  
  const [otherState, setOtherState] = useState('');
  
  // Memoized functions
  const handleUpdate = useCallback((id, newText) => {
    setItems(prevItems => 
      prevItems.map(item => 
        item.id === id ? { ...item, text: newText } : item
      )
    );
  }, []);
  
  const handleDelete = useCallback((id) => {
    setItems(prevItems => 
      prevItems.filter(item => item.id !== id)
    );
  }, []);
  
  return (
    <div>
      <input 
        value={otherState}
        onChange={(e) => setOtherState(e.target.value)}
        placeholder="Other state"
      />
      
      {/* List re-renders only when items change */}
      <List 
        items={items} 
        onUpdate={handleUpdate}
        onDelete={handleDelete}
      />
    </div>
  );
}

2. Working with nested objects

import { memo, useState } from 'react';
 
// ❌ Problem with nested objects
const UserProfileBad = memo(function UserProfileBad({ user }) {
  return (
    <div>
      <h2>{user.profile.name}</h2>
      <p>Age: {user.profile.age}</p>
      <p>Email: {user.contact.email}</p>
    </div>
  );
});
 
// ✅ Correct approach - destructuring or custom comparison
const UserProfileGood = memo(
  function UserProfileGood({ user }) {
    return (
      <div>
        <h2>{user.profile.name}</h2>
        <p>Age: {user.profile.age}</p>
        <p>Email: {user.contact.email}</p>
      </div>
    );
  },
  // Custom comparison function for nested objects
  (prevProps, nextProps) => {
    return (
      prevProps.user.profile.name === nextProps.user.profile.name &&
      prevProps.user.profile.age === nextProps.user.profile.age &&
      prevProps.user.contact.email === nextProps.user.contact.email
    );
  }
);
 
// Alternative approach - passing individual props
const UserProfileBest = memo(function UserProfileBest({ name, age, email }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
    </div>
  );
});

Common Mistakes

1. Using with mutating data

// ❌ Incorrect
class BadParent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: { value: 0 }
    };
  }
  
  handleClick = () => {
    // Mutating existing object
    this.state.data.value += 1;
    this.setState({ data: this.state.data }); // ❌ Not creating new object!
  };
  
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          Increase
        </button>
        {/* PureComponent won't re-render because it's the same object */}
        <PureChild data={this.state.data} />
      </div>
    );
  }
}
 
// ✅ Correct
class GoodParent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: { value: 0 }
    };
  }
  
  handleClick = () => {
    // Creating new object
    this.setState({
      data: { value: this.state.data.value + 1 } // ✅ New object
    });
  };
  
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          Increase
        </button>
        {/* PureComponent will re-render because it's a new object */}
        <PureChild data={this.state.data} />
      </div>
    );
  }
}

2. Creating functions in render

// ❌ Incorrect
function BadParent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      {/* New function on every render - PureComponent will re-render */}
      <PureComponent onClick={() => console.log('Click!')} />
    </div>
  );
}
 
// ✅ Correct
function GoodParent() {
  const [count, setCount] = useState(0);
  
  // Function created once
  const handleClick = useCallback(() => {
    console.log('Click!');
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Counter: {count}
      </button>
      {/* Same function on every render - PureComponent won't re-render */}
      <PureComponent onClick={handleClick} />
    </div>
  );
}

Summary

Pure components are React components that automatically optimize performance through shallow comparison of props and state:

Advantages:

  • Automatic re-render optimization
  • Performance improvement when used correctly
  • Simple implementation

Limitations:

  • Work only with immutable data
  • Shallow comparison isn’t suitable for complex objects
  • Can produce unexpected results when used incorrectly

Key points:

  • Use pure components with primitive props and immutable objects
  • For functional components, use memo
  • Avoid mutating data and functions created in render
  • When working with nested objects, use custom comparison functions

Understanding pure components helps create more performant React applications and avoid unnecessary re-renders.


Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site, and improve every day 💪