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.
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>;
}