useMemo vs useCallback.
Value vs function. Pick the right one.
Both hooks memoize. One memoizes a value; the other memoizes a function. They're the same thing under the hood — useCallback(fn, deps) is literally useMemo(() => fn, deps). But choosing between them isn't about preference; it's about what you need stable. This page shows the difference, when each matters, and when neither does.
Short answer: useMemo memoizes a value (the result of computation) — use it for expensive calculations or stable object/array identities. useCallback memoizes a function — use it for stable callback references. Both only help when the reference is consumed by something that cares (React.memo, effect dependencies, other hooks). Don't wrap everything — memoization has cost. Use it deliberately, not by default.
✅ Solution 1 — The one-line difference
Same mechanism, different return.
The simplest way to see the difference: useMemo returns the value of a function call. useCallback returns the function itself. If you need a memoized value, use useMemo. If you need a memoized function, use useCallback. They're interchangeable in principle — useCallback(fn, deps) = useMemo(() => fn, deps) — but the naming makes code clearer.
import { useMemo, useCallback, useState } from 'react'; function TheDifference({ items }: { items: number[] }) { const [count, setCount] = useState(0); // ─── useMemo: returns a VALUE ──────────────── const total = useMemo(() => { return items.reduce((sum, n) => sum + n, 0); }, [items]); // total: number — the computed value const sorted = useMemo(() => [...items].sort((a, b) => a - b), [items]); // sorted: number[] — a new array, but stable until items changes const user = useMemo(() => ({ name: 'Alice', age: 30 }), []); // user: { name: string; age: number } — stable object // ─── useCallback: returns a FUNCTION ───────── const handleClick = useCallback(() => { console.log('clicked'); }, []); // handleClick: () => void — the function itself const handleIncrement = useCallback(() => { setCount(c => c + 1); }, []); // handleIncrement: () => void — stable function // ─── Equivalent definitions ────────────────── // These two are identical: const cbA = useCallback(() => console.log('a'), []); const cbB = useMemo(() => () => console.log('a'), []); // Both return the same (() => void) function with stable identity. // ─── The distinction in one line ───────────── // useMemo: value = useMemo(() => compute(), deps) // useCallback: fn = useCallback(() => { ... }, deps) // // Same effect. Different readability. Use the one whose // name matches what you're returning. console.log(total, sorted, user, handleClick, handleIncrement, cbA, cbB, count); return (); } console.log(TheDifference);Total: {total}
Sorted: {sorted.join(', ')}
{user.name} ({user.age})
import { useMemo, useCallback, useState } from 'react'; function TheDifference({ items }) { const [count, setCount] = useState(0); const total = useMemo(() => items.reduce((sum, n) => sum + n, 0), [items]); const sorted = useMemo(() => [...items].sort((a, b) => a - b), [items]); const user = useMemo(() => ({ name: 'Alice', age: 30 }), []); const handleClick = useCallback(() => { console.log('clicked'); }, []); const handleIncrement = useCallback(() => { setCount(c => c + 1); }, []); const cbA = useCallback(() => console.log('a'), []); const cbB = useMemo(() => () => console.log('a'), []); console.log(total, sorted, user, handleClick, handleIncrement, cbA, cbB, count); return React.createElement('div', null, React.createElement('p', null, 'Total: ', total), React.createElement('p', null, 'Sorted: ', sorted.join(', ')), React.createElement('p', null, user.name, ' (', user.age, ')'), React.createElement('button', { onClick: handleIncrement }, 'Count: ', count) ); } console.log(TheDifference);
useMemo → value
const total = useMemo(() => sum(items), [items]) — total is the number, memoized until items changes.
useCallback → function
const handle = useCallback(() => ..., []) — handle is the function, memoized until deps change.
Same under the hood
useCallback(fn, deps) ≡ useMemo(() => fn, deps). Identical behavior. The name is just for clarity.
Both cache by reference
Dependencies compared by Object.is. New object/function identity → recompute. Same identity → return cached.
✅ Solution 2 — When useCallback actually helps
Only when the stable reference prevents work.
useCallback isn't a free optimization. It only helps when the stable function reference causes some other expensive thing to be skipped: a memo'd component not re-rendering, an effect not re-running, or another hook not invalidating. If nothing downstream cares about the function's identity, useCallback adds overhead with no benefit.
import { useCallback, useState, memo, useEffect } from 'react'; // ─── Case 1: memo'd child + useCallback = win ─ const MemoizedButton = memo(({ onClick }: { onClick: () => void }) => { console.log('MemoizedButton rendered'); return ; }); function GoodParent() { const [count, setCount] = useState(0); // ✅ Stable reference — MemoizedButton skips re-render const handleClick = useCallback(() => { console.log('clicked'); }, []); return ({/* Doesn't re-render when parent re-renders ✅ */}); } // ─── Case 2: effect dependency + useCallback ─ function EffectDep({ id }: { id: string }) { const load = useCallback(() => { fetch(/api/${id}`).then(r => r.json()); }, [id]); useEffect(() => { load(); }, [load]); // ✅ runs only when id changes return null; } // ─── Case 3: custom hook dependency ────────── function useDebounce(fn: () => void, delay: number) { useEffect(() => { const id = setTimeout(fn, delay); return () => clearTimeout(id); }, [fn, delay]); } function Search({ query }: { query: string }) { const handleSearch = useCallback(() => { console.log('searching:', query); }, [query]); useDebounce(handleSearch, 300); returnSearching for: {query}
; } // ─── BAD: useCallback with no consumer ─────── function PointlessCallback() { const [count, setCount] = useState(0); // ❌ Nothing depends on the identity of handleClick // No memo'd child, no effect, no custom hook. // Cost: React runs the hook every render. const handleClick = useCallback(() => { setCount(c => c + 1); }, []); // Just as fast, simpler: const handleClick2 = () => { setCount(c => c + 1); }; return (); } // ─── The rule ──────────────────────────────── // Use useCallback when: // ✅ Passing to React.memo'd component // ✅ Used in another hook's dependency array // ✅ Passed to a custom hook that depends on identity // // Don't use useCallback when: // ❌ Passing to a native DOM event (onClick, onChange) // ❌ Passing to a non-memo'd component // ❌ Only used inside the component's own render logic // // The cost: React runs a hook + compares deps every render. // The benefit: none, if nothing downstream compares identity. console.log(MemoizedButton, GoodParent, EffectDep, useDebounce, Search, PointlessCallback);
import { useCallback, useState, memo, useEffect } from 'react'; const MemoizedButton = memo(({ onClick }) => { console.log('MemoizedButton rendered'); return React.createElement('button', { onClick }, 'Click'); }); function GoodParent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log('clicked'); }, []); return React.createElement('div', null, React.createElement('button', { onClick: () => setCount(c => c + 1) }, 'Parent: ', count), React.createElement(MemoizedButton, { onClick: handleClick }) ); } function EffectDep({ id }) { const load = useCallback(() => { fetch(/api/${id}`).then(r => r.json()); }, [id]); useEffect(() => { load(); }, [load]); return null; } function useDebounce(fn, delay) { useEffect(() => { const id = setTimeout(fn, delay); return () => clearTimeout(id); }, [fn, delay]); } function Search({ query }) { const handleSearch = useCallback(() => { console.log('searching:', query); }, [query]); useDebounce(handleSearch, 300); return React.createElement('p', null, 'Searching for: ', query); } function PointlessCallback() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { setCount(c => c + 1); }, []); const handleClick2 = () => { setCount(c => c + 1); }; return React.createElement('div', null, React.createElement('button', { onClick: handleClick }, 'A: ', count), React.createElement('button', { onClick: handleClick2 }, 'B: ', count) ); } console.log(MemoizedButton, GoodParent, EffectDep, useDebounce, Search, PointlessCallback);
React.memo'd children
Stable function reference = memo skips the re-render. Without useCallback, the memo is defeated by a new function identity every render.
Effect dependencies
A function in useEffect's deps — useCallback prevents re-runs from function identity changing. Without it: infinite loop.
Custom hooks
Custom hooks that depend on function identity (useDebounce, useThrottle) need stable functions. useCallback provides them.
When it's pointless
Native event handlers (onClick), non-memo'd children, internal helpers — the function is called, not compared. No benefit from memoization.
✅ Solution 3 — When useMemo actually helps
Expensive computations and stable references.
useMemo has two use cases. Expensive computation: the calculation takes real time, and skipping it on re-render matters. Stable reference: the value (object, array) is used in an effect dependency or passed to a memo'd component. For trivial computations like a + b, useMemo is slower than just recomputing.
import { useMemo, useState, useEffect, memo } from 'react'; interface Item { id: string; price: number; name: string } // ─── Case 1: expensive computation ─────────── function ExpensiveList({ items }: { items: Item[] }) { const [filter, setFilter] = useState(''); const [sortBy, setSortBy] = useState<'price' | 'name'>('price'); // ✅ Expensive (filter + sort) — only recomputes when // items, filter, or sortBy changes. Not on every keystroke in // the filter input itself (which IS a re-render, but reuses result). const processed = useMemo(() => { return items .filter(i => i.name.toLowerCase().includes(filter.toLowerCase())) .sort((a, b) => sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)); }, [items, filter, sortBy]); return (setFilter(e.target.value)} placeholder="Filter..." />); } // ─── Case 2: stable object for a memo'd child ─ const MemoizedChart = memo(({ config }: { config: { color: string; size: number } }) => { console.log('Chart rendered'); return{processed.map(i =>
- {i.name}: $`{i.price}
)}Chart: {config.color} / {config.size}; }); function ChartParent() { const [count, setCount] = useState(0); // ✅ Stable config — MemoizedChart skips re-render when count changes const config = useMemo(() => ({ color: 'blue', size: 100 }), []); return (); } // ─── Case 3: stable object for effect dependency function StableDep({ a, b }: { a: number; b: number }) { // Without useMemo: options is new every render → effect loops const options = useMemo(() => ({ a, b }), [a, b]); useEffect(() => { console.log('options:', options); }, [options]); // ✅ runs when a or b changes return null; } // ─── BAD: memoizing trivial computations ───── function PointlessMemo({ a, b }: { a: number; b: number }) { // ❌ a + b costs nothing. useMemo has overhead. const sum = useMemo(() => a + b, [a, b]); // Just as fast, simpler: const sum2 = a + b; return{sum} / {sum2}
; } // ─── The rule ──────────────────────────────── // Use useMemo when: // ✅ Computation is expensive (sort, filter, map on large arrays) // ✅ Object/array passed to React.memo'd component // ✅ Object/array in an effect dependency // // Don't use useMemo when: // ❌ Computation is trivial (arithmetic, string concat) // ❌ Value is used only inside the component's own render logic // ❌ Nothing downstream compares identity // // Measure first. Most components don't need useMemo. console.log(ExpensiveList, MemoizedChart, ChartParent, StableDep, PointlessMemo);
import { useMemo, useState, useEffect, memo } from 'react'; function ExpensiveList({ items }) { const [filter, setFilter] = useState(''); const [sortBy, setSortBy] = useState('price'); const processed = useMemo(() => { return items .filter(i => i.name.toLowerCase().includes(filter.toLowerCase())) .sort((a, b) => sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)); }, [items, filter, sortBy]); return React.createElement('div', null, React.createElement('input', { value: filter, onChange: e => setFilter(e.target.value), placeholder: 'Filter...' }), React.createElement('button', { onClick: () => setSortBy(b => b === 'price' ? 'name' : 'price') }, 'Sort by: ', sortBy), React.createElement('ul', null, processed.map(i => React.createElement('li', { key: i.id }, i.name, ': `$', i.price))) ); } const MemoizedChart = memo(({ config }) => { console.log('Chart rendered'); return React.createElement('div', null, 'Chart: ', config.color, ' / ', config.size); }); function ChartParent() { const [count, setCount] = useState(0); const config = useMemo(() => ({ color: 'blue', size: 100 }), []); return React.createElement('div', null, React.createElement('button', { onClick: () => setCount(c => c + 1) }, 'Count: ', count), React.createElement(MemoizedChart, { config }) ); } function StableDep({ a, b }) { const options = useMemo(() => ({ a, b }), [a, b]); useEffect(() => { console.log('options:', options); }, [options]); return null; } function PointlessMemo({ a, b }) { const sum = useMemo(() => a + b, [a, b]); const sum2 = a + b; return React.createElement('p', null, sum, ' / ', sum2); } console.log(ExpensiveList, MemoizedChart, ChartParent, StableDep, PointlessMemo);
Expensive computations
Filter + sort on 10,000 items takes real time. useMemo skips it when unrelated state changes.
Stable references
config passed to memo'd child — new object every render defeats the memo. useMemo fixes identity.
Effect dependencies
A new object in useEffect's deps causes the effect to re-run every render. useMemo prevents it.
Trivial values don't need it
a + b, items.length, user.name — no memo needed. The overhead of useMemo exceeds the computation cost.
Don't use useMemo and useCallback everywhere "just in case." Every hook has cost — React runs the hook, compares dependencies, and either caches or recomputes. For trivial values and functions, this cost exceeds the savings. The optimization is only real when the reference identity prevents work elsewhere: a memo'd child skipping a render, an effect not re-running, an expensive calculation not recomputing. Add useMemo/useCallback when you've measured a problem, not preemptively.
⚠️ Common mistakes
The memoization mistakes that cost real time.
Adds cost without benefit. Fix: only memoize when the reference is consumed by something that cares.
onClick={useCallback(() => ...)} — native DOM doesn't compare identities. Fix: plain inline function.
useCallback(...) must be called unconditionally at the top of the component. Fix: not inside loops or conditions.
useCallback(() => setCount(count + 1), []) — captures stale count. Fix: setCount(c => c + 1) or add count to deps.
useMemo(() => a + b, [a, b]) — slower than a + b. Fix: only memoize when the computation actually costs.
useMemo(() => fetch(...), []) — useMemo isn't for effects. Fix: useEffect for side effects.
useMemo memoizes the value, not the render. The component still re-renders; the value is reused. Fix: combine with React.memo to prevent child renders.
— new object every render, memo fails. Fix: useMemo the object or hoist it outside.
❓ Frequently asked
What is the difference between useMemo and useCallback?
useMemo memoizes a value (the result of a function call). useCallback memoizes a function (the function itself). useMemo(() => computeValue(), [deps]) returns the computed value. useCallback(() => {...}, [deps]) returns the function. They're the same thing under the hood — useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
When should I use useCallback?
Three cases. (1) A function is passed as a prop to a component wrapped in React.memo — stable reference means the memo works. (2) A function is in the dependency array of useEffect, useMemo, or another useCallback. (3) A function is passed to a custom hook that depends on it.
When should I use useMemo?
Three cases. (1) An expensive computation runs on every render but only depends on a few values. (2) A value needs a stable identity for a useEffect dependency or a memo'd component prop. (3) A value is created inline (object, array, function) and passed to a component that would re-render on the new reference.
Is useCallback always faster?
No — useCallback has cost. Every render, React runs the hook, compares dependencies, and either returns the cached function or stores a new one. For simple functions, this cost is comparable to just creating the function inline. The benefit only appears when the stable reference prevents work elsewhere.