useCallback Hook
Cache a function definition between renders to maintain a stable reference.
Search across all documentation pages
Cache a function definition between renders to maintain a stable reference.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
handleDelete is wrapped in useCallback with [] dependencies, so its reference never changesTodoItem is wrapped in React.memo, so it only re-renders when its props changeuseCallback, typing in the input would re-render every TodoItem because handleDelete would be a new function each timehandleAdd depends on input, so it updates when the input changes - that's correctuseCallback(fn, deps) is equivalent to useMemo(() => fn, deps)Object.is| 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 |
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);
}, []);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":
count state changed.React.memo does a shallow comparison of props. onClick is the same reference (thanks to useCallback) and label is a primitive string that hasn't changed. You won't see the console.log.onClick reference 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:
useCallback makes the function reference stableReact.memo checks if props are the same (shallow equality) and skips render if they areReact.memo only does shallow comparison - if you pass objects or arrays, you need useMemo for those too// 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;
}useCallback without React.memo - Wrapping a callback in useCallback does nothing if the child is not memoized with React.memo. Fix: Only use useCallback when 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 useCallback adds cognitive overhead and memory usage. Fix: Only memoize when passing to React.memo children, or when used as a useEffect / useMemo dependency.
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 useMemo or restructure to use primitives.
| 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.
React.memo, the child re-renders on every parent render regardless of prop stability.useCallback only prevents re-renders when the consumer checks prop equality (via React.memo or dependency arrays).useCallback and add it when profiling shows unnecessary re-renders.useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).useCallback caches the function itself; useMemo caches the return value of a function.useCallback for functions and useMemo for non-function values.[], React never detects a dependency change, so it returns the same function reference forever.setState(prev => ...)) that don't depend on external values.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 changesdispatch from useReducer is always stable -- it never changes between renders.dispatch to multiple children without useCallback or React.memo concerns.const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
},
[]
);
// Type: (e: React.ChangeEvent<HTMLInputElement>) => voidObject.is sees a new reference.useMemo or restructure to use primitive values.React.memo children or using as a useEffect/useMemo dependency.useCallback is not worthwhile.const handleSelect = useCallback(
<T extends { id: string }>(item: T) => {
setSelectedId(item.id);
},
[]
);useCallback itself.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥