React useCallback Explained — Stabilize Function References
In this tutorial, you will learn about React useCallback Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
React useCallback memoizes callback functions so they maintain a stable reference between renders, preventing unnecessary re-renders of child components wrapped with React.memo.
What You'll Learn
- What useCallback does and when to use it
- How useCallback differs from useMemo
- How to use useCallback with React.memo
- Dependency rules for useCallback
- When NOT to use useCallback
Why It Matters
Without useCallback, every render creates new function instances. Child components using React.memo receive new function references and re-render unnecessarily. useCallback stabilizes references for performance-sensitive paths.
Real-World Use
Durga Antivirus Pro's ThreatTable component receives onSort, onFilter, and onRowClick callbacks. These are memoized with useCallback to prevent re-rendering the entire table when unrelated state changes.
flowchart LR
A[Parent Re-renders] --> B{useCallback?}
B -->|Yes| C[Stable Function Ref]
B -->|No| D[New Function Every Render]
C --> E[Child Memo Works]
D --> F[Child Re-renders]
style A fill:#3b82f6,color:#fff
Basic useCallback
import { useState, useCallback } from "react";
import { memo } from "react";
const SearchInput = memo(function SearchInput({ onSearch }) {
console.log("SearchInput rendered");
return (
<input
type="text"
onChange={e => onSearch(e.target.value)}
placeholder="Type to search..."
/>
);
});
function App() {
const [query, setQuery] = useState("");
const [count, setCount] = useState(0);
// Without useCallback: new function every render -> SearchInput re-renders
// With useCallback: stable reference unless query changes
const handleSearch = useCallback((value) => {
setQuery(value);
}, []); // Empty: never changes
return (
<div>
<p>Query: {query}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<SearchInput onSearch={handleSearch} />
</div>
);
}
Expected output: Incrementing count does NOT re-render SearchInput because handleSearch has a stable reference.
useCallback returns the same function instance between renders unless its dependencies change. It takes a function and a dependency array, similar to useEffect.
useCallback vs useMemo
import { useCallback, useMemo, useState } from "react";
function Example() {
const [items, setItems] = useState([1, 2, 3]);
const [filter, setFilter] = useState("");
// useCallback: returns the function itself (memoized)
const handleRemove = useCallback((id) => {
setItems(prev => prev.filter(i => i !== id));
}, []);
// useMemo: returns the RESULT of the function
const filteredItems = useMemo(() => {
return items.filter(i => i.toString().includes(filter));
}, [items, filter]);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{filteredItems.map(item => (
<button key={item} onClick={() => handleRemove(item)}>
Remove {item}
</button>
))}
</div>
);
}
Expected output: handleRemove is a stable function. filteredItems is a computed array that only recalculates when items or filter change.
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Both memoize, but useCallback returns the function while useMemo returns the result of calling the function.
Common useCallback Patterns
import { useState, useCallback } from "react";
function TodoManager() {
const [todos, setTodos] = useState([]);
const [editingId, setEditingId] = useState(null);
// Stable: no dependencies
const handleAdd = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
}, []);
// Changes when editingId changes
const handleStartEdit = useCallback((id) => {
setEditingId(id);
}, []);
// Changes when editingId changes (depends on editingId)
const handleSaveEdit = useCallback((id, newText) => {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, text: newText } : t
));
setEditingId(null);
}, []); // Note: uses functional update to avoid editingId dependency
// Stable: uses functional update
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id));
}, []);
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
isEditing={editingId === todo.id}
onStartEdit={handleStartEdit}
onSaveEdit={handleSaveEdit}
onDelete={handleDelete}
onToggle={handleToggle}
/>
))}
</div>
);
}
Expected output: All callbacks are memoized. The functional updater pattern (setTodos(prev => ...)) avoids adding state to dependency arrays.
Using functional updates lets you keep dependency arrays minimal, which means callbacks change less often and child components re-render less.
useCallback with Custom Hooks
Return memoized callbacks from custom hooks:
import { useState, useCallback } from "react";
function useCrudOperations(initialData = []) {
const [data, setData] = useState(initialData);
const add = useCallback((item) => {
setData(prev => [...prev, { ...item, id: Date.now() }]);
}, []);
const update = useCallback((id, updates) => {
setData(prev => prev.map(item =>
item.id === id ? { ...item, ...updates } : item
));
}, []);
const remove = useCallback((id) => {
setData(prev => prev.filter(item => item.id !== id));
}, []);
const reset = useCallback(() => {
setData(initialData);
}, [initialData]);
return { data, add, update, remove, reset };
}
// Usage
function ProductManager() {
const { data: products, add, update, remove } = useCrudOperations([]);
// These callbacks are stable across renders
return (
<ProductList
products={products}
onAdd={add}
onUpdate={update}
onDelete={remove}
/>
);
}
Expected output: The callbacks returned by useCrudOperations maintain stable references, making them safe to pass to memoized components.
Custom hooks that return callbacks should memoize them. Consumers of the hook do not expect the callbacks to change on every render.
When NOT to use useCallback
useCallback adds overhead and is not free:
// OVERKILL: Event handler that does not affect children
function SimpleButton({ label, onClick }) {
return <button onClick={onClick}>{label}</button>; // Not memoized
}
function App() {
const [count, setCount] = useState(0);
// UNNECESSARY: SimpleButton is not memoized, so it re-renders anyway
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <SimpleButton label="Click" onClick={handleClick} />;
}
// BETTER: Only use when child is memoized or callback is a dependency
function AppImproved() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(c => c + 1);
return <SimpleButton label="Click" onClick={handleClick} />;
}
Expected output: Both versions work identically. The non-memoized version is simpler and faster for this case.
Use useCallback only when:
- The callback is passed to a memoized child component
- The callback is used as a dependency of another hook
- Profiling shows it improves performance
Common Mistakes
Wrapping every function in useCallback — Adds unnecessary overhead. Only memoize when there is a measurable performance benefit.
Incomplete dependency arrays — Omitting dependencies causes stale closures. The linter (
react-hooks/exhaustive-deps) catches this.Unnecessary dependencies — Including functions or objects in dependencies can cause infinite loops. Use functional updates to avoid state dependencies.
Using useCallback for event handlers on DOM elements — React does not memoize event handlers for native elements. The handler does not affect re-renders.
Forgetting that useCallback does not prevent re-renders alone — It must be paired with React.memo on the child to prevent re-renders.
Practice Questions
What does useCallback do? It returns a memoized version of the callback that only changes if dependencies change.
How is useCallback different from useMemo? useCallback returns the memoized function. useMemo returns the memoized result of a function.
When should you use useCallback? When passing callbacks to memoized child components or when the callback is a dependency of another hook.
What is a stale closure? When a callback references outdated variable values because its dependency array is incomplete.
Does useCallback alone prevent child re-renders? No. The child must also use React.memo to benefit from stable callback references.
Challenge
Profile a ChatApp with message list, input box, and typing indicator. Identify which callbacks cause unnecessary re-renders of the message list. Apply useCallback to stabilize only those that improve measured performance by 20%+.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro