1. Introduction đ
useCallback is a performance-focused Hook that lets you memoize a function definition across renders. This tutorial explores how it works, its relationship to useMemo, React.memo, and useEffect, and â just as importantly â when it's better left out.
Information
2. What is useCallback? đ¤
useCallback returns a memoized version of a function that only changes if one of its dependencies has changed, preventing a new function reference from being created on every render.
Code Snippet
import { useCallback } from 'react';
function TodoList({ onAddTodo }) {
const handleAdd = useCallback((text) => {
onAddTodo({ id: crypto.randomUUID(), text });
}, [onAddTodo]);
return <TodoForm onSubmit={handleAdd} />;
}3. Why Use useCallback? đĄ
- Stable References: Prevents unnecessary re-renders in memo-wrapped child components.
- Effect Stability: Avoids re-running effects that depend on a function passed in as a dependency.
- Custom Hook Consistency: Ensures functions returned from custom Hooks remain stable for consumers.
Caution
4. Function Memoization đ§
In JavaScript, functions are reference types â every time a component renders, any function defined inside it is a brand-new object, even if its logic is identical to the previous render's version.
Code Snippet
function Component() {
// A NEW function is created on every single render
const handleClick = () => console.log("Clicked");
}Note
5. How useCallback Works âī¸
On first render, React stores the function along with its dependencies. On later renders, if the dependencies are unchanged, React returns the original function reference instead of the newly created one.
6. Syntax đ
Code Snippet
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);Tip
7. Dependency Array đ
Code Snippet
const handleSearch = useCallback((query) => {
fetchResults(query, category); // reads 'category' from outer scope
}, [category]); // must include 'category'Important
8. Memoizing Event Handlers đąī¸
Code Snippet
function ProductCard({ product, onAddToCart }) {
const handleClick = useCallback(() => {
onAddToCart(product.id);
}, [product.id, onAddToCart]);
return <button onClick={handleClick}>Add to Cart</button>;
}Tip
9. Preventing Unnecessary Re-renders đĢ
Problem: New Function Reference Breaks memo()
const ExpensiveList = memo(function ExpensiveList({ onSelect }) {
console.log("Rendering list...");
return <div>{/* expensive rendering */}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
// â New function every render â defeats memo() on ExpensiveList
const handleSelect = (id) => console.log(id);
return (
<>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveList onSelect={handleSelect} />
</>
);
}Solution: useCallback Stabilizes the Reference
function Parent() {
const [count, setCount] = useState(0);
// â
Stable reference across re-renders
const handleSelect = useCallback((id) => console.log(id), []);
return (
<>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveList onSelect={handleSelect} />
</>
);
}10. Passing Stable Callback References đ
Stable references matter most when a callback flows into multiple layers of memoized components, or into other Hooks that compare dependencies by reference.
Code Snippet
function SearchPage() {
const [results, setResults] = useState([]);
const handleSearch = useCallback((query) => {
fetchResults(query).then(setResults);
}, []); // setResults from useState is always stable
return <SearchBar onSearch={handleSearch} />;
}11. useCallback with React.memo đ§Š
React.memo skips re-rendering a component when its props are shallowly equal to the previous render's props. Without useCallback, function props defeat this check every time.
Code Snippet
const Button = memo(function Button({ onClick, label }) {
console.log("Button rendered:", label);
return <button onClick={onClick}>{label}</button>;
});
function Toolbar() {
const [count, setCount] = useState(0);
const handleSave = useCallback(() => console.log("Saving..."), []);
return (
<>
<Button onClick={handleSave} label="Save" />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
);
}
// "Save" button never re-renders when 'count' changes12. useCallback with useEffect đ
When a function is used inside a useEffect and also listed as a dependency, memoizing it with useCallback prevents the effect from re-running on every render.
Code Snippet
function ChatRoom({ roomId }) {
const connectToRoom = useCallback(() => {
return createConnection(roomId);
}, [roomId]);
useEffect(() => {
const connection = connectToRoom();
return () => connection.disconnect();
}, [connectToRoom]); // stable unless 'roomId' changes
}Tip
13. useCallback with Custom Hooks đ ī¸
Code Snippet
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, increment, decrement, reset };
}Best Practice
14. useCallback vs useMemo âī¸
| Aspect | useCallback | useMemo |
|---|---|---|
| Caches | A function reference | A computed value |
| Equivalent To | useMemo(() => fn, deps) | N/A |
| Typical Use | Stable callbacks for props or effects | Expensive calculations or derived data |
15. useCallback vs useRef âī¸
| Aspect | useCallback | useRef |
|---|---|---|
| Purpose | Memoize a function based on dependencies | Store any mutable value with no automatic updates |
| Update Trigger | Automatic, based on dependency array | Manual â you assign .current yourself |
Note
16. Performance Optimization âĄ
- useCallback itself has a small cost â the dependency comparison on every render.
- Its benefit only materializes when the stable reference actually prevents something downstream (a re-render, an effect re-run).
- Overusing it on functions with no memoized consumers adds overhead without any payoff.
Warning
17. Dependency Management đ
Code Snippet
function SearchBar({ onSearch, category }) {
const handleChange = useCallback((query) => {
onSearch(query, category); // both must be listed
}, [onSearch, category]);
}Tip
18. Stale Closures đ°ī¸
â Stale Closure â Missing Dependency
function Counter() {
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log(count); // always logs the INITIAL count
}, []); // â missing 'count' dependency
}â Fixed with the Correct Dependency
function Counter() {
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log(count); // always logs the CURRENT count
}, [count]); // â
correctly listed
}Danger
19. Common Use Cases đ¨
- Passing callbacks to memoized child components to prevent unnecessary re-renders.
- Stabilizing a function used as a dependency in useEffect.
- Returning stable functions from custom Hooks for predictable consumer behavior.
- Optimizing components in large, frequently re-rendering lists.
20. When Not to Use useCallback đĢ
- When the function is only used inside JSX event handlers on native DOM elements (like a plain button).
- When the function isn't passed to a memo-wrapped component or used as a dependency elsewhere.
- As a blanket default applied to every function "just in case."
Best Practice
21. TypeScript with useCallback đˇ
Code Snippet
interface Item {
id: string;
name: string;
}
function ItemList({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
const handleSelect = useCallback(
(id: string) => {
onSelect(id);
},
[onSelect]
);
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => handleSelect(item.id)}>{item.name}</li>
))}
</ul>
);
}Tip
22. Best Practices đ
- Use useCallback only when a stable reference genuinely prevents unnecessary work downstream.
- Pair it with React.memo on the receiving component for the optimization to actually take effect.
- Always include every value referenced inside the function in the dependency array.
- Return memoized functions from custom Hooks when consumers may use them in effects or memoized components.
- Profile before optimizing â confirm the re-render issue is real before adding useCallback.
23. Common Mistakes đĢ
- Wrapping every function in useCallback regardless of whether it provides any benefit.
- Omitting dependencies, creating stale closures that silently use outdated values.
- Using useCallback without pairing it with React.memo on the consuming component, gaining no actual benefit.
- Assuming useCallback improves performance automatically, without measuring first.
Danger
24. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
25. Summary đ
useCallback preserves a stable function reference across renders, most valuable when paired with React.memo or used as a dependency in useEffect. Like useMemo, it's a targeted performance optimization â applied deliberately after profiling, not as a universal default.