useCallback
useCallback keeps a function's identity stable between renders. See a memoized child re-render on every keystroke because of a new inline handler, fix it with useCallback, and avoid stale closures.
🎯 What you'll learn
- The basic API
- Why function identity matters
- Stable, not 'better'
- Stale closures and useCallback vs inlining
📚 Before you start
1The Basic API
useCallback(fn, deps) is useMemo specialized for one specific value: a function. It caches a function reference across re-renders, returning the exact same function identity as long as the dependency array hasn't changed:
import { useCallback, useState } from "react";
function SearchBox({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState("");
// Without useCallback, handleSubmit is a BRAND NEW function every render —
// a different reference every time, even though its behavior never changes
const handleSubmit = useCallback(() => {
onSearch(query);
}, [query, onSearch]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<SubmitButton onClick={handleSubmit} />
</div>
);
}useCallback(fn, deps) is exactly equivalent to useMemo(() => fn, deps) — same caching mechanism, same Object.is-per-element dependency comparison, just specialized so you write fn directly instead of a factory that returns fn.
2Why Function Identity Matters at All
In plain JavaScript, function identity rarely matters. In React, it matters specifically because of two consumers that check reference equality:
// 1. A dependency array of another hook
useEffect(() => {
subscribe(handleMessage);
return () => unsubscribe(handleMessage);
}, [handleMessage]); // if handleMessage is a new reference every render,
// this effect tears down and resubscribes every render
// 2. A memoized child component
const MemoizedChild = memo(function Child({ onClick }: { onClick: () => void }) {
/* ... */
});
// <MemoizedChild onClick={handleClick} /> — if handleClick is a new
// reference every render, React.memo's shallow prop comparison always
// finds a "changed" prop, and the memoization is defeatedWithout useCallback, a function declared inside a component body is a new object on every single render — closures are just objects under the hood, and every call to the enclosing function creates fresh ones. useCallback is the tool for handing a stable reference to consumers that specifically check === (or Object.is) identity.
3useCallback Does Not Make the Function "Better" — Only Stable
A common misconception is that useCallback avoids "recreating" the function's logic, as if the function body were somehow expensive to construct. Creating a function object is essentially free in JavaScript — the actual cost useCallback addresses is entirely downstream, in whatever consumes the reference:
// This function is trivial to create — useCallback here buys nothing on its
// own; it only matters if `increment` is passed to a memoized child or used
// as another hook's dependency
const increment = useCallback(() => setCount((c) => c + 1), []);
// If `increment` is never passed anywhere that checks reference equality,
// this is identical in behavior (and slightly worse in overhead) to:
const increment = () => setCount((c) => c + 1);Reach for useCallback specifically because a function is: (a) a dependency of another hook, or (b) a prop passed to a React.memo-wrapped component — not reflexively on every handler.
4Stale Closures Inside a Memoized Callback
useCallback does not change how closures work — it only decides whether a new closure is created. If the dependency array under-lists what the callback closes over, the cached function keeps closing over old values, same as any stale closure (see languages/javascript's closures material for the underlying mechanics):
function Counter() {
const [count, setCount] = useState(0);
// BUG: deps is [] but the callback reads `count` — the FIRST render's
// count (0) is trapped in this closure forever; later renders' `count`
// values are ignored because the cached function is never recreated
const logCount = useCallback(() => {
console.log(count); // always logs 0, no matter how many times count changes
}, []); // missing `count`
// FIX: list count as a dependency so the cached function is recreated
// (and its closure refreshed) whenever count changes
const logCountFixed = useCallback(() => {
console.log(count);
}, [count]);
}The functional-updater form (setCount(prev => ...), covered in the useState topic) often sidesteps this entirely by not needing to read the outer count at all — the safest fix for a callback that only needs to update state, not read it.
5useCallback vs Inlining
Just like useMemo, useCallback should be applied where it solves a specific reference-identity problem, not by default on every function in a component:
function Panel() {
// No useCallback needed: this handler isn't passed to a memoized child
// and isn't any other hook's dependency — a plain inline function is fine
return <button onClick={() => console.log("clicked")}>Click</button>;
}Wrapping every handler in useCallback "just in case" adds bookkeeping overhead (the dependency comparison itself) without a matching benefit, and it also invites Pitfall #1 below (missing dependencies) for no gain. See the Performance optimization topic for measuring whether memoization is worth it in a given case.
Under the HoodHow it works internally▾
React does not attach state to the component function — a function component is just a function, called fresh every render with no persistent memory of its own. Hook data lives on the fiber, React's internal object representing one component instance, specifically in a singly linked list of hook objects hanging off the fiber's memoizedState pointer — one node per hook call, walked in the exact order hooks were called, every render. That fixed call-order requirement is why hooks (including useCallback) can never be called conditionally.
useCallback's hook node stores the same two things a useMemo node stores: a cached value (here, a function reference) and the dependency array from the render that produced it. In fact, inside React's own source, useCallback(fn, deps) is implemented as a thin wrapper that calls the same internal mechanism as useMemo(() => fn, deps) — there is no separate "callback caching" machinery. On each render, React compares the new dependency array against the stored one element-by-element with Object.is; if everything matches, the node returns the previously stored function reference without evaluating fn again (there's nothing to "evaluate" — fn itself, as written in your source, is a fresh closure every render regardless; what useCallback skips is returning that fresh one and instead hands back the old one). If any dependency changed, React stores and returns this render's fn, which becomes the new cached reference going forward.
This clarifies exactly what's "created" and what's "reused": your component function body still constructs a brand-new closure object every single render — JavaScript does that unconditionally, useCallback cannot prevent it. What useCallback controls is which one of those closures actually gets returned to the rest of your component and handed to props/dependency arrays — the one from this render, or the previously cached one from a past render. A discarded closure that was never returned is simply garbage collected; it does no work and has no further effect.
Like useMemo, this caching is explicitly a performance optimization, not a correctness guarantee: React may discard a memoized callback in scenarios like reclaiming memory for offscreen trees, so component logic must remain correct even if a "new" function happened to be created and used every render.
⚠️ Common Pitfalls
- Missing a closed-over dependency — the cached function keeps its closure from whenever it was last recreated, silently reading stale values (a direct instance of stale-closure semantics — see
languages/javascript's closures material) until a listed dependency finally changes. - Using
useCallbackwithoutReact.memoon the receiving component — reference stability alone doesn't stop a child from re-rendering;React.memo's shallow prop comparison is what actually benefits from the stable reference. - Wrapping every handler "just in case" — adds a dependency-comparison cost for no benefit when the function isn't a memoized child's prop or another hook's dependency.
- Passing an inline arrow function as one of the dependencies (e.g. a new
() => {}in the array) — the same inline-literal trap asuseMemo; it's neverObject.is-equal to the previous render's, so the cache never hits. - Assuming
useCallbackavoids recreating the closure object — it doesn't; the closure is created every render regardless.useCallbackonly decides which one gets returned.
📋 Quick Reference
const fn = useCallback(() => doSomething(a, b), [a, b])
→ equivalent to useMemo(() => (() => doSomething(a, b)), [a, b])
→ returns the SAME function reference while a, b are unchanged
Use when: the function is (a) a React.memo child's prop, or
(b) another hook's dependency array entry
Skip when: the function isn't consumed by anything that checks identity
Missing a dependency → stale closure, same rules as useEffect/useMemo🧠 Key takeaways
useCallback(fn, deps)=useMemo(() => fn, deps): a stable function reference.- It matters when the function is a prop of a memo child or a dependency of another hook.
- Missing deps cause stale closures (old state); use functional updates or include deps.
- Otherwise, inline functions are fine; the React Compiler can handle this automatically.
🎤 Interview Q&A
Q1What does useCallback do?core▾
Returns a memoized function whose identity stays the same between renders unless dependencies change: useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
- Does it make the function faster? No; it only preserves identity.
Q2When is useCallback useful?core▾
When passing callbacks to memoized children (React.memo) or when a function is a dependency of an effect or another hook, to avoid re-renders or re-running effects.
- When useless? Passing to plain DOM elements or non-memoized children.
Q3How do stale closures happen with useCallback?core▾
Missing dependencies keep the callback bound to old state/props. Include all dependencies, use functional state updates, or refs for latest values.
- Lint rule? react-hooks/exhaustive-deps.
Q4How do you keep a stable callback that always sees the latest state?senior▾
Store state in a ref or use the 'latest ref' pattern / useEffectEvent, so the callback identity is stable while reading current values.
- Risk? Reading refs during render breaks purity; only read them in handlers/effects.
Q5useCallback in custom hooks?core▾
Custom hooks that return functions should usually memoize them so consumers can safely use them in dependencies.
- Example? A
useDebouncereturning a stable debounced function.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.