React Hooks — useCallback & useMemo
Difficulty: 🟡 Medium | Frequency: ⭐⭐⭐⭐⭐
Core Distinction
useCallback— memoizes a function referenceuseMemo— memoizes a computed value
// useCallback: returns the function itself
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
// useMemo: returns the result of calling the function
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// useCallback(fn, deps) === useMemo(() => fn, deps)
Why They Exist: Referential Equality
JavaScript compares functions and objects by reference, not value. Every render creates new references:
function Parent() {
const handleClick = () => console.log('clicked'); // NEW function each render
const options = { theme: 'dark' }; // NEW object each render
return <Child onClick={handleClick} options={options} />;
// Child re-renders even if nothing relevant changed!
}
useCallback and useMemo stabilize references so memoized children can bail out.
useCallback
// ❌ Without: new function every render → breaks React.memo on child
const handleDelete = (id) => {
setItems(prev => prev.filter(item => item.id !== id));
};
// ✅ With: stable reference → React.memo works
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // No deps because we use functional update
When to Use
- Passing callbacks to memoized child components (
React.memo) - In dependency arrays of other hooks
Dependency Array Patterns
// ✅ Functional update avoids state dependency
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []); // Not: [count]
// ✅ Use refs for values you need but don't want as deps
const latestData = useRef(data);
useEffect(() => { latestData.current = data; }, [data]);
const handleSubmit = useCallback(() => {
process(latestData.current); // Read from ref
}, []); // Stable reference
useMemo
// ❌ Expensive calculation runs on every render
const sortedUsers = users
.filter(u => u.active)
.sort((a, b) => a.name.localeCompare(b.name));
// ✅ Only recalculates when users changes
const sortedUsers = useMemo(() => {
return users
.filter(u => u.active)
.sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
Common Use Cases
// 1. Expensive calculations
const total = useMemo(() =>
items.reduce((sum, item) => sum + item.price * item.qty, 0),
[items]
);
// 2. Stable object references (for context or props)
const contextValue = useMemo(() => ({
user,
login,
logout
}), [user]);
// 3. Derived data from props
const filtered = useMemo(() =>
products.filter(p => p.category === selectedCategory),
[products, selectedCategory]
);
React.memo + useCallback/useMemo = No Unnecessary Re-renders
// Child only re-renders if its props change
const ExpensiveList = React.memo(function ExpensiveList({ items, onDelete }) {
console.log('ExpensiveList rendered');
return items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
));
});
function Parent({ items }) {
const [count, setCount] = useState(0);
// Stable reference — ExpensiveList won't re-render when count changes
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveList items={items} onDelete={handleDelete} />
</>
);
}
The Memoization Paradox — Don't Over-Optimize
Memoization adds memory overhead and comparison cost. It's harmful when:
- The component is cheap to render
- Props change every render anyway (negates memoization)
- You add it "just in case" without profiling
Only memoize when profiling confirms a bottleneck (render > 16ms).
// ❌ Unnecessary: cheap computation
const doubled = useMemo(() => count * 2, [count]);
const doubled = count * 2; // ✅ Just compute it
// ✅ Necessary: 5000-item sort on every render
const sorted = useMemo(() => largeArray.sort(compareFunc), [largeArray]);
Real-World Impact
Dashboard search feature before optimization: 800–1200ms input lag. After strategic useCallback + useMemo: 16–32ms (50× improvement).
Root cause: Parent re-rendered on every keystroke, creating new function references, breaking memoization on all chart components, triggering 247 component renders per keystroke.
Content from Frontend-Master-Prep-Series — 03-react