React useMemo and useCallback
Optimize component performance by memoizing expensive calculations with useMemo and stable function references with useCallback.
TL;DR
- Cache heavy computation results across render cycles using the
useMemohook. - Preserve stable function identity across re-renders with the
useCallbackhook. - Prevent downstream component re-renders by pairing memoized callbacks with
memo.
Caching Values with useMemo
Expensive Filter and SortCache filtered array computations until source data changes.
const visibleItems = useMemo(() => {
return items
.filter((i) => i.price <= maxPrice)
.sort((a, b) => a.price - b.price);
}, [items, maxPrice]);Stable Object ReferencePrevent child re-renders caused by new object literals.
const options = useMemo(() => {
return { theme: isDark ? 'dark' : 'light' };
}, [isDark]);Heavy Mathematical CalculationMemoize complex statistical transforms or graph metrics.
const stats = useMemo(() => {
return computeComplexStats(dataset);
}, [dataset]);Stable Functions with useCallback
Child Action HandlerKeep callback reference stable to prevent memoized child re-renders.
const handleDelete = useCallback(
(id: string) => {
setItems((prev) =>
prev.filter((it) => it.id !== id)
);
},
[]
);Effect Dependency CallbackPrevent infinite effect re-runs by memoizing handler prop.
const fetchData = useCallback(async () => {
const res = await api.get(url);
setData(res);
}, [url]);Debounced CallbackStabilize debounced functions across component renders.
const debouncedSearch = useCallback(
debounce((q: string) => search(q), 300),
[]
);Pairing with React.memo
Memoized Child ComponentSkip re-rendering child when incoming props remain unchanged.
export const ItemRow = memo(function ItemRow({
item,
onSelect,
}: ItemRowProps) {
return (
<RowView title={item.name} onClick={onSelect} />
);
});Custom Comparison FunctionSupply custom comparison predicate for deep prop checks.
export const Card = memo(CardView, (prev, next) => {
return prev.item.id === next.item.id;
});Children Invalidation NoticePassing inline JSX as children always busts React.memo.
// WARNING: inline children breaks memo
<MemoChild><Text>Dynamic</Text></MemoChild>Optimization Pitfalls
Premature OptimizationAdding hooks to basic math costs more than it saves.
// AVOID: unnecessary useMemo
const total = useMemo(() => a + b, [a, b]);Missing DependenciesOmitting variables captures stale closure values.
// BAD: filterValue omitted from deps
const filter = useCallback(() => {
return query(val);
}, []);React Compiler FutureModern toolchains automate memoization during build.
// React Compiler automatically memoizes
const list = items.filter(fn);Tips
- Profile before memoizing; avoid wrapping cheap operations in
useMemobecause hook overhead exceeds calculation cost. - Pass callbacks created with
useCallbackinto child components optimized withReact.memoto prevent unwanted re-renders.
Warnings
- Omitting reactive values from the dependency array produces stale calculations and broken event handlers in
useCallback. - Do not rely on
useMemoas a semantic guarantee; React may release cached values to free memory under high resource pressure.
In Practice
Combine useMemo for list filtering with useCallback to preserve row render boundaries.
- Filter and sort large product catalogs inside useMemo.
- Stabilize item deletion handler using useCallback with functional setter.
- Pass stable callbacks into memoized row child components.
- Prevent unnecessary row renders when search input text changes.
export function Catalog({ list }: { list: Item[] }) {
const [query, setQuery] = useState('');
const items = useMemo(() => {
return list.filter((i) => i.name.includes(query));
}, [list, query]);
const select = useCallback((id: string) => {
console.log('Select:', id);
}, []);
return (
<ListView>
<InputBase value={query} onChange={setQuery} />
{items.map((it) => (
<MemoRow key={it.id} it={it} onPick={select} />
))}
</ListView>
);
}FAQ
useMemo executes its factory function and caches the returned calculated value, while useCallback caches the function reference itself without invoking it during render.
No. useCallback has memory and comparison overhead. It only improves performance when passing callbacks to memoized child components or effect dependency arrays.
The React Compiler automatically memoizes component outputs, expressions, and callback identities at build time, reducing the necessity of manual useMemo and useCallback hooks in modern codebases.