How does useCallback work in React?
The Short Answer
useCallback returns a memoized version of a callback function that only changes if its dependencies change. It prevents creating a new function reference on every render, which matters when you pass callbacks to child components wrapped in React.memo — without useCallback, the child would re-render every time because it receives a "new" function (even though the logic is identical).
The Problem It Solves
In React, every time a component re-renders, all functions defined inside it are recreated. This means a new function object is created in memory with a new reference — even if the function body is exactly the same. For most cases this is fine and cheap. But it becomes a problem when you pass that function as a prop to a memoized child component.
The example below shows the problem. ExpensiveList is wrapped in React.memo to skip re-renders when props haven't changed. But because handleDelete is recreated on every render of Parent, it's a new reference every time — and memo's shallow comparison sees it as a changed prop, defeating the optimization entirely.
const ExpensiveList = memo(function ExpensiveList({
items,
onDelete,
}: {
items: string[];
onDelete: (id: string) => void;
}) {
console.log('ExpensiveList rendered'); // Logs on EVERY parent render 😩
return (
<ul>
{items.map(item => (
<li key={item}>
{item} <button onClick={() => onDelete(item)}>Delete</button>
</li>
))}
</ul>
);
});
function Parent() {
const [query, setQuery] = useState('');
const [items] = useState(['apple', 'banana', 'cherry']);
// ❌ New function reference on every render
// memo on ExpensiveList is useless because onDelete always "changes"
const handleDelete = (id: string) => {
console.log('delete', id);
};
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ExpensiveList items={items} onDelete={handleDelete} />
</div>
);
}
The Solution: useCallback
useCallback memoizes the function reference. React stores the function and returns the same reference on subsequent renders — unless one of the specified dependencies changes. Now React.memo on the child actually works because it receives the same function reference between renders.
function Parent() {
const [query, setQuery] = useState('');
const [items, setItems] = useState(['apple', 'banana', 'cherry']);
// ✅ Same reference between renders (unless setItems changes — it won't)
const handleDelete = useCallback((id: string) => {
setItems(prev => prev.filter(item => item !== id));
}, []); // Empty deps — setItems is stable, no external values used
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* Now memo works — handleDelete is the same reference */}
<ExpensiveList items={items} onDelete={handleDelete} />
</div>
);
}
When the user types in the input, Parent re-renders. But handleDelete keeps the same reference (dependencies haven't changed), so ExpensiveList skips re-rendering because both items and onDelete are unchanged. The expensive list only re-renders when items actually change.
How It Works Internally
On the first render, useCallback stores the function and its dependency array. On subsequent renders, React compares the current dependencies with the stored ones (shallow comparison). If they're the same, it returns the stored function. If any dependency changed, it stores the new function and returns that instead.
// useCallback is essentially this:
function useCallback(fn, deps) {
// First render: store fn and deps
// Subsequent renders:
if (depsAreSame(previousDeps, deps)) {
return previousFn; // Same reference — skip update
} else {
previousFn = fn;
previousDeps = deps;
return fn; // New reference — dependency changed
}
}
// useCallback(fn, deps) is equivalent to:
// useMemo(() => fn, deps)
// The difference is semantic — useCallback is for functions, useMemo for values
Dependencies Matter
The dependency array tells React when the function needs to be recreated. If your callback uses values from the component scope (props, state, derived values), those must be in the dependency array. Otherwise the callback captures stale values from a previous render — a closure bug.
function SearchResults({ query, filters }: { query: string; filters: Filter[] }) {
// ❌ Stale closure — query and filters are captured from first render
const handleSearch = useCallback(() => {
fetchResults(query, filters);
}, []); // Missing dependencies!
// ✅ Correct — recreates when query or filters change
const handleSearch = useCallback(() => {
fetchResults(query, filters);
}, [query, filters]);
// ✅ Alternative — use functional updates to avoid dependencies
const handleDelete = useCallback((id: string) => {
// Using the setter's callback form avoids needing 'items' in deps
setItems(prev => prev.filter(item => item.id !== id));
}, []); // No external values referenced — empty deps is correct
}
The stale closure trap
If your callback reads state or props but you omit them from dependencies, the function "remembers" the values from when it was created — not the current values. This is the #1 bug with useCallback. The ESLint exhaustive-deps rule catches this.
When NOT to Use useCallback
useCallback is not free — it adds complexity and has its own overhead (storing the function, comparing dependencies). It's only beneficial when the stable reference actually prevents work. In many cases, it's premature optimization that makes code harder to read for zero performance gain.
Don't use useCallback when
- ❌The child component is NOT wrapped in React.memo — a stable reference doesn't help if the child re-renders anyway
- ❌The function is passed to native DOM elements — `<button onClick={fn}>` doesn't benefit from stable references
- ❌The dependencies change on every render — useCallback recreates the function anyway, adding overhead for nothing
- ❌The component is simple and re-renders are cheap — the optimization cost exceeds the savings
useCallback + React.memo Pattern
The canonical pattern is: wrap the child in React.memo, then use useCallback for any function props you pass to it. Both pieces are required — useCallback without memo on the child is pointless, and memo without useCallback for function props is defeated.
// Step 1: Memoize the child component
const TodoItem = memo(function TodoItem({
todo,
onToggle,
onDelete,
}: {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
}) {
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>×</button>
</li>
);
});
// Step 2: Stabilize function props with useCallback
function TodoList() {
const [todos, setTodos] = useState<Todo[]>(initialTodos);
const handleToggle = useCallback((id: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []);
const handleDelete = useCallback((id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}
Now when one todo is toggled, only that specific TodoItem re-renders (because its todo prop changed). All other items skip re-rendering because their todo, onToggle, and onDelete props are all the same references.
Why Interviewers Ask This
This question tests whether you understand React's rendering model, referential equality, and when memoization actually helps. Interviewers want to see that you know why functions get recreated on every render, how that interacts with React.memo, when useCallback is beneficial vs wasteful, and how to handle dependencies correctly. It separates developers who optimize thoughtfully from those who sprinkle useCallback everywhere without understanding the mechanism.
Quick Revision Cheat Sheet
Purpose: Memoize a function reference so it stays the same between renders
When useful: Passing callbacks to React.memo children or as useEffect dependencies
When useless: Child isn't memoized, or deps change every render anyway
Dependencies: Must include all values from component scope used inside the callback
Pattern: useCallback (parent) + React.memo (child) = skip unnecessary re-renders
Equivalent to: useMemo(() => fn, deps) — but semantically clearer for functions