useCallback
useCallback returns a memoised version of a function. The function is only recreated when its dependencies change. Without useCallback, a new function object is created on every render, which matters when you pass that function to child components wrapped in React.memo.
Why function references matter
Every time a component renders, any functions defined inside it are recreated as new objects. Two functions with identical code are still different objects in JavaScript:
const a = () => {};
const b = () => {};
console.log(a === b); // false
This becomes a problem when you pass a function as a prop to a child component you have wrapped in React.memo. React.memo skips re-rendering when props have not changed. But if the parent re-renders for any reason (like unrelated state changing), it creates a new function object, and React.memo sees a new prop value and re-renders anyway.
import { memo, useState } from 'react';
// React.memo: only re-renders if props change
const MemoizedChild = memo(function MemoizedChild({ onRemove }) {
console.log('Child rendered');
return <button onClick={onRemove}>Remove</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState(['apple', 'banana', 'cherry']);
// New function object on every render
const handleRemove = () => {
setItems(prev => prev.slice(1));
};
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild onRemove={handleRemove} />
</div>
);
}
Every time the count button is clicked, Parent re-renders, handleRemove is a new function, and MemoizedChild re-renders despite the memo wrapper. Clicking count has nothing to do with the child.
Using useCallback
Wrap handleRemove in useCallback to keep the same function reference between renders:
import { memo, useState, useCallback } from 'react';
const MemoizedChild = memo(function MemoizedChild({ onRemove }) {
console.log('Child rendered');
return <button onClick={onRemove}>Remove</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState(['apple', 'banana', 'cherry']);
// Same function reference until items changes
const handleRemove = useCallback(() => {
setItems(prev => prev.slice(1));
}, []); // no dependencies: this function does not read any external values
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild onRemove={handleRemove} />
</div>
);
}
Now clicking the count button re-renders Parent, but handleRemove is the same function object, so MemoizedChild does not re-render.
The dependency array works the same way as useEffect: list any values from the component scope that the function uses. If those values change, the function is recreated.
A practical full example
A todo list where removing an item should not re-render every other item:
import { memo, useState, useCallback } from 'react';
const TodoItem = memo(function TodoItem({ todo, onRemove }) {
console.log(`Rendered: ${todo.text}`);
return (
<li>
{todo.text}
<button onClick={() => onRemove(todo.id)}>Remove</button>
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Buy groceries' },
{ id: 2, text: 'Walk the dog' },
{ id: 3, text: 'Read a book' },
]);
const [filter, setFilter] = useState('');
const removeTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []); // setTodos is stable, no other deps needed
const filtered = todos.filter(todo =>
todo.text.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter todos..."
/>
<ul>
{filtered.map(todo => (
<TodoItem key={todo.id} todo={todo} onRemove={removeTodo} />
))}
</ul>
</div>
);
}
export default TodoList;
As you type in the filter input, TodoList re-renders on every keystroke. Without useCallback, each TodoItem would also re-render. With useCallback, removeTodo stays the same reference, so React.memo on TodoItem can do its job.
useCallback vs useMemo
Both hooks cache something between renders:
| Hook | Caches | Returns |
|---|---|---|
useMemo | The result of calling a function | A value |
useCallback | The function itself | A function |
// useMemo: caches the computed value
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// useCallback: caches the function
const handleSort = useCallback(() => {
setItems(prev => [...prev].sort((a, b) => a.name.localeCompare(b.name)));
}, []);
In fact, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). They are the same mechanism; useCallback is just a convenience wrapper for the common case of caching a function.
When NOT to use useCallback
Most of the time, you do not need useCallback. Every call has a cost: React has to store the previous function, compare dependencies, and decide whether to return the cached version. If there is no React.memo child consuming the function, this cost buys you nothing.
Add useCallback only when both of these are true:
- The function is passed as a prop to a child wrapped in
React.memo. - The parent re-renders frequently for reasons unrelated to that child.
Do not add it speculatively. If you suspect a performance problem, use the React DevTools Profiler to identify the bottleneck before adding memoisation.
Common mistakes
Missing a dependency.
If your function reads a value from the component scope and you leave it out of the dependency array, the function will use a stale version of that value:
// Bug: if userId changes, deleteUser still has the old value
const deleteUser = useCallback(() => {
api.delete(userId); // stale userId if not in deps
}, []); // should be [userId]
Using useCallback on every function.
Adding useCallback to functions that are never passed to memoised children gives you the overhead of memoisation with none of the benefit. Keep it targeted.
Forgetting React.memo on the child.
useCallback alone does not prevent the child from re-rendering. It only gives the child a stable function reference. The child still needs React.memo (or useMemo for objects) to use that stable reference as a skip signal.