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);
}, []);React.memo + useCallback Full Example
A complete, runnable example that clearly demonstrates how React.memo works together with useCallback.
"use client";
import React, { useState, useCallback } from "react";
// 1. Memoized Child Component
const MemoizedChild = React.memo(function MemoizedChild({
onClick,
label,
}: {
onClick: () => void;
label: string;
}) {
console.log(`${label} rendered`); // ← Watch the console
return (
<button onClick={onClick} className="border rounded px-3 py-1">
{label}
</button>
);
});
// 2. Regular (non-memoized) Child for comparison
function RegularChild({
onClick,
label,
}: {
onClick: () => void;
label: string;
}) {
console.log(`${label} (regular) rendered`);
return (
<button onClick={onClick} className="border rounded px-3 py-1">
{label} (regular)
</button>
);
}
export default function Parent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
// Stable callback thanks to useCallback
const handleClick = useCallback(() => {
alert("Button clicked!");
}, []); // empty dependency array → never changes
return (
<div className="space-y-4 p-4">
<h2 className="text-lg font-bold">Parent Component</h2>
<p>Count: {count}</p>
<div className="flex gap-2">
<button
onClick={() => setCount((c) => c + 1)}
className="border rounded px-3 py-1"
>
Increment Count (causes parent re-render)
</button>
<button
onClick={() => setOtherState((s) => s + 1)}
className="border rounded px-3 py-1"
>
Update Other State ({otherState})
</button>
</div>
<div className="space-y-2">
<h3 className="font-semibold">Memoized Children</h3>
<div className="flex gap-2">
<MemoizedChild onClick={handleClick} label="Memoized Button 1" />
<MemoizedChild onClick={handleClick} label="Memoized Button 2" />
</div>
<h3 className="font-semibold">Regular (non-memoized) Children</h3>
<RegularChild onClick={handleClick} label="Regular Button" />
</div>
</div>
);
}What happens when you click "Increment Count":
- Parent re-renders because its
countstate changed. - MemoizedChild does NOT re-render —
React.memodoes a shallow comparison of props.onClickis the same reference (thanks touseCallback) andlabelis a primitive string that hasn't changed. You won't see the console.log. - RegularChild DOES re-render — normal functional components always re-render when their parent does, even though the
onClickreference is stable.
Without useCallback (what usually goes wrong):
If you remove useCallback and write const handleClick = () => { alert("Button clicked!"); }, then even MemoizedChild re-renders on every parent render because a new function is created each time, causing the prop comparison to fail.
Key takeaways:
useCallbackmakes the function reference stableReact.memochecks if props are the same (shallow equality) and skips render if they are- Both are needed together for the optimization to work when passing callbacks
React.memoonly does shallow comparison — if you pass objects or arrays, you needuseMemofor those too
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