Both hooks do the same underlying thing — skip redoing work if the inputs haven't changed since last time — applied to two different kinds of value: a calculated result, and a function.
A component function re-runs on every render, which means anything computed inside it gets recomputed every time too — including work that's genuinely expensive and didn't need to change:
function ProductList({ products, filter }) {
const filtered = products.filter((p) => p.category === filter); // recalculated every render
return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}useMemo caches a calculated value, only recomputing it when something in the dependency array (the same pattern from useEffect) has actually changed:
function ProductList({ products, filter }) {
const filtered = useMemo(
() => products.filter((p) => p.category === filter),
[products, filter]
);
return <ul>{filtered.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}A new function is created on every render too, by default — usually harmless, but it matters when that function is passed as a prop to a memoized child component (via React.memo, from the Performance Basics lesson next), since a "new" function prop looks like a change even when the logic is identical. useCallback keeps the same function reference across renders unless its dependencies change:
function Parent({ items }) {
const handleSelect = useCallback((id) => {
console.log('Selected:', id);
}, []); // same function reference every render
return <ExpensiveList items={items} onSelect={handleSelect} />;
}Both add real overhead of their own — comparing the dependency array, storing the cached value. For most components, that overhead outweighs any saving, since most calculations and function creations are cheap. Reach for either specifically when a profiler has actually shown a real slowdown, or you know a calculation is genuinely expensive (sorting or filtering a large list) — not as a default habit applied to every value and function.
Not a default habit
Wrapping everything in useMemo/useCallback "just in case" is a common overcorrection — it adds real complexity and its own small performance cost for a benefit that, in most components, doesn't exist. Measure before optimizing.