Memoize deliberately
Use useMemo, useCallback, and memo where measurement shows expensive work or wasted re-renders, and structure the code so that memoization actually takes effect.
Implementation
- Do not memoize cheap expressions with primitive results, such as
a || borcount * 2; the Hook's bookkeeping costs more than the expression. - Give each memoized computation only the dependencies it uses.
Split a
useMemooruseEffectthat combines independent work with different dependencies. - Keep the props of a
memocomponent referentially stable. Define default values for object, array, and function props as module-level constants rather than inline defaults. - To skip expensive work when an early return makes it unnecessary, move the work into a child component and render the child only when needed.
- When the project uses React Compiler, let it handle memoization and do not add manual memoization for performance.
Rationale
Memoization trades memory and comparison work for skipped computation. It pays off only when the skipped work is expensive and the dependencies rarely change. A new object or function created on every render, such as an inline default prop, changes a dependency every time and quietly disables the memoization.
Examples
Application: A cheap expression
Incorrect (counterexample):
const isLoading = useMemo(() => user.isLoading || notifications.isLoading, [user.isLoading, notifications.isLoading]);
Correct:
const isLoading = user.isLoading || notifications.isLoading;
Application: A default prop on a memoized component
Incorrect (counterexample):
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
// ...
});
When callers omit onClick, each render creates a new default function, so memo never skips a render.
Correct:
const noop = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = noop }: { onClick?: () => void }) {
// ...
});
Application: Independent work in one Hook
Incorrect (counterexample):
const sortedProducts = useMemo(() => {
const filtered = products.filter((product) => product.category === category);
return filtered.toSorted((a, b) => (sortOrder === 'asc' ? a.price - b.price : b.price - a.price));
}, [products, category, sortOrder]);
Changing the sort order also repeats the filtering.
Correct:
const filteredProducts = useMemo(
() => products.filter((product) => product.category === category),
[products, category],
);
const sortedProducts = useMemo(
() => filteredProducts.toSorted((a, b) => (sortOrder === 'asc' ? a.price - b.price : b.price - a.price)),
[filteredProducts, sortOrder],
);
Validation
Profile with the React DevTools Profiler before and after adding memoization, and keep it only where it reduces render time or re-renders.
Check that props passed to memo components do not change identity on every render.
Missing memoization in a project that uses React Compiler is not a violation.