useCallback returns a memoized version of a function that only changes when
one of its dependencies changes. Calling it with the same dependencies returns the
exact same function reference across renders.
const handleClick = useCallback(() => {
doSomething(id)
}, [id]) // new function only when `id` changes
Without useCallback, a new function is created on every render. With it, the
reference stays stable until id changes — which matters when passing the
function to a memoized child or an effect dependency.
Rule of thumb: useCallback(fn, deps) is useMemo(() => fn, deps).
useMemo caches the return value of a computation and recomputes it only when
its dependencies change.
const sorted = useMemo(
() => [...items].sort(compare),
[items] // recompute only when items changes
)
On renders where items hasn't changed, React skips calling the sort and returns
the cached result. Use it for expensive derivations from props or state.
They solve the same problem — referential stability across renders — for different output types:
useMemomemoizes the return value of a function (any value).useCallbackmemoizes the function itself (a specific case of useMemo).
// these two are equivalent
const fn = useCallback(() => compute(x), [x])
const fn = useMemo(() => () => compute(x), [x])
// useMemo for a computed value
const result = useMemo(() => compute(x), [x])
// useCallback for a stable function reference
const handler = useCallback(() => post(x), [x])
In practice: useCallback for functions, useMemo for values.
JavaScript compares objects and functions by reference (memory address), not by content. Two identically-written functions are not equal unless they're the same reference.
(() => {}) === (() => {}) // false — different references
const fn = () => {}
fn === fn // true — same reference
This is why a new function created each render makes a child re-render even when
nothing logically changed: React.memo sees a new prop reference and treats it
as a change. useCallback returns the same reference, keeping memoization intact.
React.memo wraps a component and skips re-rendering when all its props are
reference-equal to the last render. If you pass an unstable function prop
(new reference each render), React.memo still re-renders — defeating the memo.
const Button = React.memo(function Button({ onClick }) {
return <button onClick={onClick}>click</button>
})
function Parent() {
// new function each render -> Button re-renders despite memo
return <Button onClick={() => console.log('hi')} />
// stable function -> memo works
const handleClick = useCallback(() => console.log('hi'), [])
return <Button onClick={handleClick} />
}
The pattern: React.memo on the child + useCallback on the function prop
passed from the parent.
Memoize when you have a measurable performance problem, not preemptively. The three cases worth memoizing:
- Expensive computation — sorting/filtering thousands of items, complex math.
- Stable reference for a memoized child —
React.memochild that receives functions or objects as props. - Effect dependency stabilization — an object/function listed in
useEffectdeps that would otherwise re-run the effect every render.
// worth it: sort runs O(n log n) on a large list
const sorted = useMemo(() => items.sort(compare), [items])
// not worth it: trivial, memo overhead exceeds savings
const doubled = useMemo(() => x * 2, [x])
Profile first. Every useMemo/useCallback adds its own comparison cost and
cognitive overhead — it's not free.
useMemo and useCallback have their own cost: React must store the cached
value, compare dependency arrays on each render, and manage garbage collection.
Memoizing cheap operations can make performance worse because the overhead
exceeds the computation.
// overhead > savings for trivial computation
const label = useMemo(() => `Hello ${name}`, [name])
// just compute it: const label = `Hello ${name}`
Additionally, memoization adds cognitive load — developers reading the code must reason about whether the deps array is correct. Default to plain computation; add memoization only when profiling shows a real problem.
The memoized value/function closes over a stale value — the one from the render where it was last created. It never sees later updates to the omitted dep.
// user is omitted from deps -> greeting always shows the initial user.name
const greeting = useMemo(() => `Hi ${user.name}`, []) // stale closure!
// correct
const greeting = useMemo(() => `Hi ${user.name}`, [user.name])
The react-hooks/exhaustive-deps eslint rule catches this. Never suppress it to
"cache something forever" — use a useRef if you intentionally want a value
frozen after mount.
A function defined in a component is recreated each render (new reference). If
an effect depends on it, the effect re-runs every render. useCallback gives the
function a stable reference that only changes when its own deps do.
// without useCallback: new function each render -> effect fires every render
function fetchUser() { return api.get(userId) }
useEffect(() => { fetchUser().then(setUser) }, [fetchUser])
// with useCallback: stable function -> effect fires only when userId changes
const fetchUser = useCallback(() => api.get(userId), [userId])
useEffect(() => { fetchUser().then(setUser) }, [fetchUser])
Alternatively, move the function inside the effect to remove the dependency entirely — that's often cleaner.
Wrap the object in useMemo so it keeps the same reference until its inputs
change. Without it, every render creates a new object even if the content is
identical — causing memoized children and dependent effects to fire.
// new object every render -> child always re-renders
<Chart options={{ color: theme, size }} />
// stable reference -> child only re-renders when theme or size change
const options = useMemo(() => ({ color: theme, size }), [theme, size])
<Chart options={options} />
This is the object-version of useCallback. Prefer depending on the individual
primitives directly when possible — it's simpler.
Compute the derived value with useMemo, depending on the raw data and any sort/
filter parameters. The computation is skipped on renders where none of those
inputs changed.
const filtered = useMemo(
() => products
.filter(p => p.category === activeCategory)
.sort((a, b) => a.price - b.price),
[products, activeCategory]
)
Without useMemo, the filter and sort run on every keystroke, click, and hover
that triggers a re-render — even when products and activeCategory haven't
changed.
Each call to useCallback/useMemo on every render:
- Reads the stored deps array.
- Compares each dep with
Object.is. - Either returns the cached value or re-runs the computation.
For a tiny list or a trivial function, this comparison can cost more than just re-running the computation. React's own team has noted that memoization is a micro-optimization and can be counter-productive on small values.
// comparison overhead likely exceeds multiplication cost
const n = useMemo(() => x * 2, [x])
// vs just: const n = x * 2
Benchmark before memoizing. Use React DevTools Profiler to identify actual expensive renders, then target them specifically.
No. Inline functions are perfectly fine when:
- The child is not wrapped in
React.memo. - The function is not in an effect dependency array.
- The child re-render is cheap (no deep tree, no expensive computation).
// fine: Button is not memoized, re-render is cheap
<Button onClick={() => setOpen(true)} />
// needed: DataGrid is memoized and re-renders are expensive
const handleSort = useCallback(col => setSort(col), [])
<DataGrid onSort={handleSort} />
Defaulting to useCallback for every handler is premature optimization that adds
noise. Add it when you have a real memoization need.
Always prefer useMemo over storing derived data in state. State requires you to
keep two values in sync (the source and the derived copy), which can lead to
bugs. useMemo derives the value automatically and stays in sync by definition.
// anti-pattern: derived state
const [items, setItems] = useState([])
const [count, setCount] = useState(0)
// must remember to call setCount every time setItems changes
// correct: derive in render
const [items, setItems] = useState([])
const count = useMemo(() => items.length, [items])
// or even simpler: const count = items.length (no memo needed here)
For truly expensive derivations, add useMemo; for trivial ones, just compute
them inline — no hook needed.
Remove memoization when: (1) profiling shows the memoized path is never hot; (2) the dependency array is so complex or unstable that the memo rarely hits; (3) the memoized value is consumed by a non-memoized child (the stability gains nothing).
// useless: MutableChild re-renders anyway for other reasons
const handler = useCallback(fn, [deps])
<MutableChild onClick={handler} />
// useful: MemoChild skips renders when handler is stable
const handler = useCallback(fn, [deps])
<MemoChild onClick={handler} />
Memoization is not free — when it doesn't help, it only adds confusion. Keep the code simple; add memos with evidence, remove them without regret.
More Hooks interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.