Problem Solved

React · useMemo vs useCallback

Home › Courses › React › Problems › useMemo vs useCallback
⚛️ Problem · 5 min read

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.

the-difference.tsx
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 (

Total: {total}

Sorted: {sorted.join(', ')}

{user.name} ({user.age})

); } console.log(TheDifference);
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.

usecallback-when.tsx
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); return

Searching 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.

usememo-when.tsx
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..." />
    {processed.map(i =>
  • {i.name}: $`{i.price}
  • )}
); } // ─── Case 2: stable object for a memo'd child ─ const MemoizedChart = memo(({ config }: { config: { color: string; size: number } }) => { console.log('Chart rendered'); return
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.

1. Wrapping every function in useCallback

Adds cost without benefit. Fix: only memoize when the reference is consumed by something that cares.

2. Using useCallback for native events

onClick={useCallback(() => ...)} — native DOM doesn't compare identities. Fix: plain inline function.

3. Forgetting that useCallback is a hook

useCallback(...) must be called unconditionally at the top of the component. Fix: not inside loops or conditions.

4. Wrong dependencies

useCallback(() => setCount(count + 1), []) — captures stale count. Fix: setCount(c => c + 1) or add count to deps.

5. Memoizing trivial values

useMemo(() => a + b, [a, b]) — slower than a + b. Fix: only memoize when the computation actually costs.

6. Using useMemo for side effects

useMemo(() => fetch(...), []) — useMemo isn't for effects. Fix: useEffect for side effects.

7. Expecting useMemo to prevent all re-renders

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.

8. Not memoizing when passing to memo'd child

— 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.

🔗 Related

← All React problems
Problem solved · React