Frontend / React / pure_components.md

React Pure Components

Updated 3 interview angles 10 min read source
On this page10
  1. Table of Contents
  2. What are Pure Components?
  3. React.memo (Functional Components)
  4. PureComponent (Class Components)
  5. Performance Benefits
  6. Shallow Comparison
  7. Best Practices
  8. Common Interview Questions
  9. Summary
  10. Interview angle

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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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 3

  • “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.