React.memo and Memoization

Difficulty: Advanced

React.memo is a higher-order component that wraps a functional component and prevents it from re-rendering if its props have not changed. By default, React re-renders a child component whenever its parent re-renders, even if the child's props are identical. React.memo adds a shallow comparison of the previous and next props - if all props are the same by reference, the render is skipped.

Shallow comparison means React checks each prop with Object.is (essentially ===). Primitives (strings, numbers, booleans) are compared by value, so they work naturally. But objects, arrays, and functions are compared by reference. If a parent creates a new object or function on every render (which is the default behavior), the shallow comparison will always see 'new' props and React.memo will have no effect. This is the most common mistake.

To make React.memo effective with non-primitive props, you need useMemo (for objects/arrays) and useCallback (for functions). useMemo returns a memoized value that only recomputes when its dependencies change. useCallback returns a memoized function reference that stays stable across renders. Together with React.memo, they form a complete memoization strategy.

However, memoization is not free. It adds memory overhead (storing previous values), CPU overhead (running comparisons), and code complexity. You should only memoize when you have measured a performance problem. The React Profiler or React DevTools Highlight Updates feature can show you which components re-render and how long they take. Memoize the expensive ones, not everything.

A good rule of thumb: use React.memo for components that (1) render often due to parent updates, (2) render the same output for the same props most of the time, and (3) are medium-to-large in complexity. Small components (a paragraph, a simple button) re-render so cheaply that memoization overhead exceeds the benefit.

Code examples

React.memo preventing unnecessary re-renders

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});

function Parent() {
  const [count, setCount] = React.useState(0);
  const items = React.useMemo(() => [
    { id: 1, name: 'Item A' },
    { id: 2, name: 'Item B' },
  ], []); // stable reference

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <ExpensiveList items={items} />
    </div>
  );
}

React.memo checks if items changed. Because items is memoized with useMemo and has no dependencies, it keeps the same reference. The shallow comparison passes, and ExpensiveList skips re-rendering.

useCallback for stable function references

const SearchInput = React.memo(function SearchInput({ value, onChange }) {
  console.log('SearchInput rendered');
  return <input value={value} onChange={onChange} />;
});

function SearchPage() {
  const [query, setQuery] = React.useState('');
  const [results, setResults] = React.useState([]);

  // Without useCallback: onChange is a new function every render
  // With useCallback: onChange is stable
  const handleChange = React.useCallback((e) => {
    setQuery(e.target.value);
  }, []);

  return (
    <div>
      <SearchInput value={query} onChange={handleChange} />
      <ResultsList results={results} />
    </div>
  );
}

Without useCallback, handleChange would be a new function on every render, causing React.memo to see different props every time. useCallback keeps the same reference.

Custom comparison function

const UserCard = React.memo(
  function UserCard({ user, onSelect }) {
    console.log('UserCard rendered for:', user.name);
    return (
      <div onClick={() => onSelect(user.id)}>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    );
  },
  // Custom comparator: only re-render if user.id changes
  (prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);

// Even if the user object reference changes, UserCard won't re-render
// as long as user.id stays the same.

The second argument to React.memo is a custom comparison function. Return true to skip re-render (props are equal), false to re-render. This gives fine-grained control beyond shallow comparison.

Key points

Concepts covered

React.memo, useMemo, useCallback, shallow comparison, referential equality, memoization tradeoffs