frontend / react / pure_components.md

React Pure Components

3 interview angles 10 min read source

React Pure Components

Pure components are a performance optimization technique in React that helps prevent unnecessary re-renders by implementing shallow comparison of props and state.

Table of Contents


What are Pure Components?

Pure components are React components that only re-render when their props or state actually change. They implement a shallow comparison to determine if a re-render is necessary.

Characteristics of Pure Components:

  • Shallow Comparison: Compare props and state using shallow equality
  • Performance Optimization: Prevent unnecessary re-renders
  • Predictable: Only re-render when data actually changes
  • Immutable Data: Work best with immutable data patterns

When to Use Pure Components:

  • Components that receive the same props frequently
  • Components that are expensive to render
  • Components in lists or grids
  • Components that don’t need frequent updates

React.memo (Functional Components)

React.memo is a higher-order component that memoizes functional components, preventing re-renders when props haven’t changed.

Basic Usage:

import React from 'react';

// Regular functional component
function UserCard({ user, onDelete }) {
  console.log('UserCard rendered');

  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onDelete(user.id)}>Delete</button>
    </div>
  );
}

// Memoized version
const MemoizedUserCard = React.memo(UserCard);

// Usage
function UserList({ users, onDeleteUser }) {
  return (
    <div>
      {users.map(user => (
        <MemoizedUserCard
          key={user.id}
          user={user}
          onDelete={onDeleteUser}
        />
      ))}
    </div>
  );
}

Custom Comparison Function:

import React from 'react';

function ExpensiveComponent({ data, config }) {
  // Expensive rendering logic
  return (
    <div>
      {/* Complex rendering */}
    </div>
  );
}

// Custom comparison function
const areEqual = (prevProps, nextProps) => {
  // Only re-render if data changed, ignore config changes
  return prevProps.data === nextProps.data;
};

const MemoizedExpensiveComponent = React.memo(ExpensiveComponent, areEqual);

// Usage
function App() {
  const [data, setData] = useState([]);
  const [config, setConfig] = useState({ theme: 'light' });

  return (
    <div>
      <MemoizedExpensiveComponent data={data} config={config} />
      <button onClick={() => setConfig({ theme: 'dark' })}>
        Change Theme
      </button>
    </div>
  );
}

With Inline Functions:

import React, { useCallback } from 'react';

function UserCard({ user, onDelete }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <button onClick={() => onDelete(user.id)}>Delete</button>
    </div>
  );
}

const MemoizedUserCard = React.memo(UserCard);

// Parent component
function UserList({ users }) {
  // Bad: Inline function creates new reference on every render
  const handleDelete = (id) => {
    console.log('Deleting user:', id);
  };

  return (
    <div>
      {users.map(user => (
        <MemoizedUserCard
          key={user.id}
          user={user}
          onDelete={handleDelete} // New function reference every time
        />
      ))}
    </div>
  );
}

// Good: Use useCallback to memoize the function
function UserList({ users }) {
  const handleDelete = useCallback((id) => {
    console.log('Deleting user:', id);
  }, []); // Empty dependency array since it doesn't depend on any values

  return (
    <div>
      {users.map(user => (
        <MemoizedUserCard
          key={user.id}
          user={user}
          onDelete={handleDelete} // Same function reference
        />
      ))}
    </div>
  );
}

PureComponent (Class Components)

PureComponent is the class component equivalent of React.memo. It implements shouldComponentUpdate with shallow comparison.

Basic Usage:

import React from 'react';

// Regular class component
class UserCard extends React.Component {
  render() {
    console.log('UserCard rendered');

    const { user, onDelete } = this.props;

    return (
      <div className="user-card">
        <h3>{user.name}</h3>
        <p>{user.email}</p>
        <button onClick={() => onDelete(user.id)}>Delete</button>
      </div>
    );
  }
}

// Pure component version
class PureUserCard extends React.PureComponent {
  render() {
    console.log('PureUserCard rendered');

    const { user, onDelete } = this.props;

    return (
      <div className="user-card">
        <h3>{user.name}</h3>
        <p>{user.email}</p>
        <button onClick={() => onDelete(user.id)}>Delete</button>
      </div>
    );
  }
}

// Usage
class UserList extends React.Component {
  render() {
    const { users, onDeleteUser } = this.props;

    return (
      <div>
        {users.map(user => (
          <PureUserCard
            key={user.id}
            user={user}
            onDelete={onDeleteUser}
          />
        ))}
      </div>
    );
  }
}

Custom shouldComponentUpdate:

class ExpensiveComponent extends React.PureComponent {
  shouldComponentUpdate(nextProps, nextState) {
    // Custom comparison logic
    const { data, config } = this.props;
    const { data: nextData, config: nextConfig } = nextProps;

    // Only re-render if data changed, ignore config changes
    return data !== nextData;
  }

  render() {
    const { data, config } = this.props;

    return (
      <div>
        {/* Expensive rendering based on data */}
        {data.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    );
  }
}

Performance Benefits

Before Pure Components:

function ParentComponent() {
  const [count, setCount] = useState(0);
  const users = [
    { id: 1, name: 'John', email: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' }
  ];

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment Count
      </button>

      {/* These components re-render every time count changes */}
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

After Pure Components:

function ParentComponent() {
  const [count, setCount] = useState(0);
  const users = [
    { id: 1, name: 'John', email: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' }
  ];

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment Count
      </button>

      {/* These components only re-render when their props change */}
      {users.map(user => (
        <MemoizedUserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

const MemoizedUserCard = React.memo(UserCard);

Performance Measurement:

import React, { useState, useEffect } from 'react';

function PerformanceTest() {
  const [count, setCount] = useState(0);
  const [renderCount, setRenderCount] = useState(0);

  useEffect(() => {
    setRenderCount(prev => prev + 1);
  });

  return (
    <div>
      <p>Parent rendered: {renderCount} times</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Update Count
      </button>

      <MemoizedExpensiveComponent data={{ value: 'static' }} />
    </div>
  );
}

const MemoizedExpensiveComponent = React.memo(ExpensiveComponent);

function ExpensiveComponent({ data }) {
  const [renderCount, setRenderCount] = useState(0);

  useEffect(() => {
    setRenderCount(prev => prev + 1);
  });

  // Simulate expensive operation
  const expensiveCalculation = () => {
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.random();
    }
    return result;
  };

  return (
    <div>
      <p>ExpensiveComponent rendered: {renderCount} times</p>
      <p>Expensive calculation result: {expensiveCalculation()}</p>
    </div>
  );
}

Shallow Comparison

How Shallow Comparison Works:

// Shallow comparison checks if references are the same
const shallowEqual = (obj1, obj2) => {
  if (obj1 === obj2) return true;

  if (typeof obj1 !== 'object' || obj1 === null ||
      typeof obj2 !== 'object' || obj2 === null) {
    return false;
  }

  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) return false;

  for (let key of keys1) {
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }

  return true;
};

Examples of Shallow Comparison:

// These will be considered equal (same reference)
const obj1 = { name: 'John', age: 30 };
const obj2 = obj1;
shallowEqual(obj1, obj2); // true

// These will be considered different (different references)
const obj1 = { name: 'John', age: 30 };
const obj2 = { name: 'John', age: 30 };
shallowEqual(obj1, obj2); // false

// Arrays with same content but different references
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
shallowEqual(arr1, arr2); // false

// Same array reference
const arr1 = [1, 2, 3];
const arr2 = arr1;
shallowEqual(arr1, arr2); // true

Common Pitfalls:

// Bad: Creating new objects/arrays in render
function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Update Count
      </button>

      {/* New object created on every render */}
      <MemoizedChild data={{ value: 'static' }} />

      {/* New array created on every render */}
      <MemoizedList items={[1, 2, 3]} />
    </div>
  );
}

// Good: Use useMemo for expensive objects/arrays
function ParentComponent() {
  const [count, setCount] = useState(0);

  const staticData = useMemo(() => ({ value: 'static' }), []);
  const staticItems = useMemo(() => [1, 2, 3], []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Update Count
      </button>

      <MemoizedChild data={staticData} />
      <MemoizedList items={staticItems} />
    </div>
  );
}

Best Practices

1. Use Pure Components for Expensive Rendering:

// Good: Memoize expensive components
const ExpensiveChart = React.memo(({ data, config }) => {
  // Expensive chart rendering
  return (
    <div className="chart">
      {/* Complex chart visualization */}
    </div>
  );
});

// Usage
function Dashboard({ metrics }) {
  const [theme, setTheme] = useState('light');

  return (
    <div>
      <ExpensiveChart data={metrics} config={{ theme }} />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

2. Memoize Callback Functions:

// Good: Use useCallback for event handlers
function UserList({ users, onDeleteUser }) {
  const handleDelete = useCallback((id) => {
    onDeleteUser(id);
  }, [onDeleteUser]);

  return (
    <div>
      {users.map(user => (
        <MemoizedUserCard
          key={user.id}
          user={user}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

3. Memoize Expensive Calculations:

// Good: Use useMemo for expensive calculations
function DataTable({ data, filters }) {
  const filteredData = useMemo(() => {
    return data.filter(item => {
      // Expensive filtering logic
      return filters.every(filter => filter.test(item));
    });
  }, [data, filters]);

  return (
    <table>
      {filteredData.map(item => (
        <MemoizedTableRow key={item.id} item={item} />
      ))}
    </table>
  );
}

const MemoizedTableRow = React.memo(TableRow);

4. Avoid Pure Components for Simple Components:

// Unnecessary: Simple component doesn't benefit from memoization
const SimpleText = React.memo(({ text }) => <p>{text}</p>);

// Better: Keep it simple
const SimpleText = ({ text }) => <p>{text}</p>;

5. Use Keys Properly:

// Good: Stable keys for list items
function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <MemoizedUserCard
          key={user.id} // Stable key
          user={user}
        />
      ))}
    </div>
  );
}

// Bad: Unstable keys
function UserList({ users }) {
  return (
    <div>
      {users.map((user, index) => (
        <MemoizedUserCard
          key={index} // Unstable key
          user={user}
        />
      ))}
    </div>
  );
}

Common Interview Questions

Q: What is a pure component in React?

  • A pure component is a component that only re-renders when its props or state actually change, using shallow comparison.

Q: What is the difference between React.memo and PureComponent?

  • React.memo is for functional components, while PureComponent is for class components. Both implement shallow comparison.

Q: How does shallow comparison work?

  • Shallow comparison checks if the references of props and state are the same, not their content.

Q: When should you use pure components?

  • Use pure components for expensive components, components that receive the same props frequently, or components in lists.

Q: What are the limitations of pure components?

  • They only do shallow comparison, so they won’t detect changes in nested objects or arrays with the same reference.

Q: How do you handle callback functions with pure components?

  • Use useCallback to memoize callback functions so they maintain the same reference between renders.

Q: Can you override the comparison logic in pure components?

  • Yes, React.memo accepts a custom comparison function as the second argument.

Q: What is the performance impact of pure components?

  • Pure components can significantly improve performance by preventing unnecessary re-renders, especially for expensive components.

Q: When should you NOT use pure components?

  • For simple components where the overhead of comparison exceeds the benefit, or when props change frequently.

Summary

  • Pure components prevent unnecessary re-renders using shallow comparison
  • React.memo is for functional components, PureComponent is for class components
  • Shallow comparison checks reference equality, not deep content equality
  • Use pure components for expensive rendering, list items, and components with stable props
  • useCallback and useMemo help maintain stable references for pure components
  • Pure components work best with immutable data patterns
  • Understanding when and how to use pure components is crucial for React performance optimization

Interview angle

  • “What is a pure component?” - one that renders the same output for the same props and state, with no side effects during render. React.PureComponent and React.memo add a shallow props comparison to skip re-rendering when nothing changed.
  • “Why does React.memo so often fail to help?” - shallow comparison. An inline object, array or arrow function prop is a new reference every render, so the comparison always fails. That is what useMemo/useCallback were for on the parent side.
  • “Does any of this still matter with React Compiler?” - much less. The compiler memoizes automatically, so hand-written memo/useMemo/useCallback is largely redundant in new code and adds noise. Purity is still required - the compiler assumes it, and mutating props or state during render produces genuinely wrong output. See react_compiler.md.