Why does a component re-render?
React re-renders a component when:
- Its state changes (a
setState). - Its props change.
- Its parent component re-renders (even if its props don't change!).
That third point is key: by default, if a parent re-renders, all of its children re-render in a cascade. Almost always that is cheap and nothing happens. But when a subtree is heavy, it's useful to have tools to avoid unnecessary work.
React.memo: memoizes a component
React.memo wraps a component so that it only re-renders if its props
change (shallow comparison). If the parent repaints but the child's props are
the same, React reuses the previous render:
const HeavyList = React.memo(function HeavyList({ items }) {
// expensive render...
return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>;
});
It's only useful if the props stay stable between renders. That's where the next two hooks come in.
useMemo: memoizes a computed value
useMemo remembers the result of a computation and only recomputes it if
any of its dependencies change. Ideal for expensive computations:
const sorted = useMemo(() => {
return [...items].sort((a, b) => a - b); // expensive computation
}, [items]); // only recomputes if 'items' changes
Without useMemo, that sort would run on every render. With it, only
when items changes.
useCallback: memoizes a function
On every render, new functions are created. If you pass a function as a prop
to a component wrapped in React.memo, that prop "changes" every time and the
memoization breaks. useCallback stabilizes the identity of the function:
const handleClick = useCallback(() => {
setSelection(id);
}, [id]); // same function as long as 'id' doesn't change
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps): one
memoizes the function, the other its result.
Mental rule:
useMemofor values,useCallbackfor functions,React.memofor components. All three compare dependencies/props to decide whether they can skip work.
Examples
useMemo avoids recomputing if the dependencies don't change
let computations = 0;
function expensiveCompute(n) { computations++; return n * 2; }
// Render 1
const a = expensiveCompute(21);
// Render 2 with the same input -> with useMemo it would NOT recompute
console.log("result:", a, "| times computed:", computations);