Optimizing React Performance with useMemo and useCallback
Published on May 8, 2025
React's useMemo
and useCallback
hooks are powerful tools for optimizing performance, but they're often misused. Let's explore when and how to use them effectively.
Understanding the Problem
React re-renders components when their props or state change. Sometimes, expensive calculations or function recreations can cause performance bottlenecks.
useMemo: Memoizing Expensive Calculations
useMemo
is used to memoize the result of expensive calculations:
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState('');
// Without useMemo, this calculation runs on every render
const expensiveValue = useMemo(() => {
console.log('Calculating expensive value...');
return items
.filter(item => item.name.includes(filter))
.reduce((sum, item) => sum + item.value, 0);
}, [items, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<p>Total value: {expensiveValue}</p>
</div>
);
}
useCallback: Memoizing Functions
useCallback
is used to memoize functions, preventing unnecessary re-renders of child components:
import { useCallback, useState, memo } from 'react';
// Child component wrapped in memo
const ChildComponent = memo(({ onClick, title }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>{title}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Without useCallback, this function is recreated on every render
const handleClick = useCallback(() => {
console.log('Button clicked!');
}, []); // Empty dependency array means this function never changes
const incrementCount = useCallback(() => {
setCount(prev => prev + 1);
}, []); // This is safe because we use the updater function
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name..."
/>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} title="Click me" />
<ChildComponent onClick={incrementCount} title="Increment" />
</div>
);
}
Common Mistakes to Avoid
1. Overusing useMemo and useCallback
Don't wrap everything in useMemo
or useCallback
. They have their own overhead:
// ❌ Bad: Unnecessary memoization
const simpleValue = useMemo(() => props.value * 2, [props.value]);
// ✅ Good: Simple calculation, no memoization needed
const simpleValue = props.value * 2;
2. Wrong Dependencies
Always include all dependencies in the dependency array:
// ❌ Bad: Missing dependency
const calculateTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price * taxRate, 0);
}, [items]); // Missing taxRate!
// ✅ Good: All dependencies included
const calculateTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price * taxRate, 0);
}, [items, taxRate]);
When to Use These Hooks
Use useMemo when:
- You have expensive calculations that don't need to run on every render
- You're creating objects or arrays that are passed as props to memoized components
- You're breaking referential equality unnecessarily
Use useCallback when:
- You're passing functions as props to memoized child components
- The function is a dependency of other hooks
- You're creating event handlers in loops
Measuring Performance
Always measure the actual performance impact:
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log('Component:', id, 'Phase:', phase, 'Duration:', actualDuration);
}
function App() {
return (
<Profiler id="ExpensiveComponent" onRender={onRenderCallback}>
<ExpensiveComponent />
</Profiler>
);
}
Alternative: React.memo
Sometimes React.memo
is all you need:
const OptimizedComponent = memo(function MyComponent({ title, onClick }) {
return <button onClick={onClick}>{title}</button>;
});
// Or with custom comparison
const OptimizedComponent = memo(function MyComponent(props) {
return <div>{/* component content */}</div>;
}, (prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.title === nextProps.title;
});
Remember: premature optimization is the root of all evil. Use these hooks when you have identified actual performance problems, not preemptively.