useCallback Hook
Cache a function definition between renders to maintain a stable reference.
Recipe
Quick-reference recipe card — copy-paste ready.
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// Common pattern: stabilize a callback passed to a memoized child
const handleDelete = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);When to reach for this: You pass a function as a prop to a React.memo child, or as a dependency in a useEffect or useMemo, and need it to not change on every render.
Working Example
"use client";
import { memo, useCallback, useState } from "react";
interface TodoItemProps {
id: number;
text: string;
onDelete: (id: number) => void;
}
const TodoItem = memo(function TodoItem({ id, text, onDelete }: TodoItemProps) {
console.log(`Rendering: ${text}`);
return (
<li className="flex items-center justify-between py-1">
<span>{text}</span>
<button onClick={() => onDelete(id)} className="text-red-500 text-sm">
Delete
</button>
</li>
);
});
export function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React" },
{ id: 2, text: "Build an app" },
{ id: 3, text: "Ship it" },
]);
const [input, setInput] = useState("");
const handleDelete = useCallback((id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const handleAdd = useCallback(() => {
if (!input.trim()) return;
setTodos((prev) => [...prev, { id: Date.now(), text: input }]);
setInput("");
}, [input]);
return (
<div className="space-y-2">
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="border rounded px-2 py-1 flex-1"
placeholder="New todo"
/>
<button onClick={handleAdd} className="px-3 py-1 border rounded">
Add
</button>
</div>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} id={todo.id} text={todo.text} onDelete={handleDelete} />
))}
</ul>
</div>
);
}What this demonstrates:
handleDeleteis wrapped inuseCallbackwith[]dependencies, so its reference never changesTodoItemis wrapped inReact.memo, so it only re-renders when its props change- Without
useCallback, typing in the input would re-render everyTodoItembecausehandleDeletewould be a new function each time handleAdddepends oninput, so it updates when the input changes — that's correct
Deep Dive
How It Works
useCallback(fn, deps)is equivalent touseMemo(() => fn, deps)- On the first render, React stores the function and its dependencies
- On subsequent renders, React compares each dependency using
Object.is - If all dependencies are unchanged, React returns the previously stored function (same reference)
- If any dependency changed, React stores and returns the new function
Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
fn | (...args: A) => R | The function to memoize |
dependencies | unknown[] | Array of reactive values used inside the function |
| Return | Type | Description |
|---|---|---|
memoizedFn | (...args: A) => R | Cached function with a stable reference |
Variations
Stabilize an event handler for useEffect:
const fetchData = useCallback(async () => {
const res = await fetch(`/api/items?page=${page}`);
setData(await res.json());
}, [page]);
useEffect(() => {
fetchData();
}, [fetchData]);With generics:
const handleSelect = useCallback(<T extends { id: string }>(item: T) => {
setSelectedId(item.id);
}, []);Stable callback with no dependencies (common for updater patterns):
const toggle = useCallback(() => {
setOpen((prev) => !prev);
}, []);TypeScript Notes
// TypeScript infers the callback type from usage
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
}, []);
// handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
// When passing to a child, the child's prop type constrains the callback type
interface Props {
onSelect: (id: string) => void;
}Gotchas
-
useCallback without React.memo — Wrapping a callback in
useCallbackdoes nothing if the child is not memoized withReact.memo. Fix: Only useuseCallbackwhen the consumer actually benefits from a stable reference. -
Stale closure — If you omit a dependency, the callback closes over an old value. Fix: Include all reactive values in the dependency array, or use updater functions (
setState(prev => ...)) to avoid the dependency. -
Over-memoizing — Wrapping every function in
useCallbackadds cognitive overhead and memory usage. Fix: Only memoize when passing toReact.memochildren, or when used as auseEffect/useMemodependency. -
Dependencies that change every render — If a dependency is an unstable object or array, the callback reference changes every render anyway. Fix: Memoize the dependency with
useMemoor restructure to use primitives.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Inline function | Child is not memoized, or the function is only used in the same component | Function is passed to a React.memo child |
useMemo | You need to memoize a non-function value | You need to memoize a function |
useReducer dispatch | Multiple children need to trigger state changes — dispatch is always stable | Simple single-value updates |
| Ref callback | You need a stable function that always reads the latest values | You want the function identity to change when deps change |
Rule of thumb: Start without useCallback. Add it when you profile and find unnecessary re-renders in memoized children, or when a function used as an effect dependency keeps re-triggering the effect.
FAQs
Does useCallback do anything if the child component is not wrapped in React.memo?
- No. Without
React.memo, the child re-renders on every parent render regardless of prop stability. useCallbackonly prevents re-renders when the consumer checks prop equality (viaReact.memoor dependency arrays).- Start without
useCallbackand add it when profiling shows unnecessary re-renders.
How is useCallback related to useMemo?
useCallback(fn, deps)is equivalent touseMemo(() => fn, deps).useCallbackcaches the function itself;useMemocaches the return value of a function.- Use
useCallbackfor functions anduseMemofor non-function values.
Why does useCallback with an empty dependency array [] create a stable reference?
- With
[], React never detects a dependency change, so it returns the same function reference forever. - This is safe when the callback uses only updater functions (
setState(prev => ...)) that don't depend on external values. - If the callback reads state or props directly, those values become stale.
Gotcha: What happens if I omit a dependency from useCallback?
- The callback closes over the stale value from the render when it was created.
- This causes bugs where the callback reads outdated state or props.
- Include all reactive values in the dependency array, or use updater functions to avoid the dependency.
When should I use useCallback to stabilize a function for useEffect?
const fetchData = useCallback(async () => {
const res = await fetch(`/api/items?page=${page}`);
setData(await res.json());
}, [page]);
useEffect(() => {
fetchData();
}, [fetchData]); // Only re-runs when page changesWhy would useReducer's dispatch be a better alternative than useCallback in some cases?
dispatchfromuseReduceris always stable -- it never changes between renders.- You can pass
dispatchto multiple children withoutuseCallbackorReact.memoconcerns. - It's ideal when multiple children need to trigger different state changes.
How does TypeScript infer the type of a useCallback function?
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
},
[]
);
// Type: (e: React.ChangeEvent<HTMLInputElement>) => void- TypeScript infers the callback type from the function signature you provide.
Gotcha: Why does my useCallback reference change every render even with dependencies?
- One of the dependencies is an unstable reference (a new object or array created each render).
- Even though the values inside the object haven't changed,
Object.issees a new reference. - Memoize the dependency with
useMemoor restructure to use primitive values.
Should I wrap every event handler in useCallback?
- No. Wrapping every function adds cognitive overhead and memory usage.
- Only memoize when passing to
React.memochildren or using as auseEffect/useMemodependency. - For inline handlers on native elements, the overhead of
useCallbackis not worthwhile.
How do I use useCallback with a generic type parameter in TypeScript?
const handleSelect = useCallback(
<T extends { id: string }>(item: T) => {
setSelectedId(item.id);
},
[]
);- The generic is placed on the inner function, not on
useCallbackitself.
Related
- useMemo — memoize computed values rather than functions
- useEffect — stable callbacks prevent unnecessary effect re-runs
- useRef — alternative for always-current values without re-render triggers
- Custom Hooks — custom hooks often return memoized callbacks