Chapter 2 of 4

memo and Referential Equality

How React.memo works, and the reason it so often does nothing.

By default, when a component re-renders, all of its children re-render too - regardless of whether their props changed. React.memo opts a component out of that: it re-renders only when its props actually differ.

const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
  return items.map((item) => <Row key={item.id} item={item} onSelect={onSelect} />);
});

The comparison is shallow

memo compares each prop with Object.is. Primitives compare by value, but objects, arrays and functions compare by reference - and a fresh one is created on every render of the parent.

memo here is pure overhead: both props differ every time.
function Parent() {
  const [count, setCount] = useState(0);

  // New array and new function on every render
  return (
    <ExpensiveList
      items={data.filter((item) => item.active)}
      onSelect={(id) => console.log(id)}
    />
  );
}

Making the props stable

function Parent() {
  const [count, setCount] = useState(0);

  const items = useMemo(() => data.filter((item) => item.active), [data]);
  const handleSelect = useCallback((id) => console.log(id), []);

  return <ExpensiveList items={items} onSelect={handleSelect} />;
}

A cheaper option: pass children instead

A component passed as children is created by the grandparent, so it does not re-render when the middle component's state changes. No memoisation needed.

// Every Counter render re-renders ExpensiveTree
function Counter() {
  const [count, setCount] = useState(0);
  return <div onClick={() => setCount(count + 1)}><ExpensiveTree /></div>;
}

// ExpensiveTree is created by the parent, so it is untouched by count
function Counter({ children }) {
  const [count, setCount] = useState(0);
  return <div onClick={() => setCount(count + 1)}>{children}</div>;
}