React’s useMemo
and useCallback
hooks are essential tools for optimizing performance in React applications by preventing unnecessary computations and re-renders. Let’s explore how to use them effectively.
useMemo
– Memoizing Expensive Calculations
Basic Usage
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
When to Use
- For expensive calculations that don’t need to run on every render
- When you need to maintain referential equality between renders
Real-World Examples
1. Filtering/Sorting Large Lists
function UserList({ users, searchTerm }) {
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. Complex Object Transformations
function Dashboard({ metrics }) {
const chartData = useMemo(() => {
return {
labels: metrics.map(m => m.date),
datasets: [{
data: metrics.map(m => m.value),
// ... other complex chart config
}]
};
}, [metrics]);
return <Chart data={chartData} />;
}
3. Component Memoization
const ExpensiveComponent = React.memo(function({ complexConfig }) {
// Renders based on complexConfig
});
function Parent({ props }) {
const config = useMemo(() => {
return { /* complex object creation */ };
}, [props.dependency]);
return <ExpensiveComponent complexConfig={config} />;
}
useCallback
– Memoizing Functions
Basic Usage
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
When to Use
- When passing callbacks to optimized child components that rely on reference equality
- When functions are dependencies in other hooks like
useEffect
Real-World Examples
1. Preventing Child Component Re-renders
const Child = React.memo(function({ onClick }) {
// Only re-renders when onClick reference changes
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies = stable reference
return <Child onClick={handleClick} />;
}
2. Event Handlers with Dependencies
function ProductPage({ productId }) {
const [quantity, setQuantity] = useState(1);
const addToCart = useCallback(() => {
api.addToCart(productId, quantity);
}, [productId, quantity]); // Only recreates when these change
return (
<button onClick={addToCart}>
Add to Cart
</button>
);
}
3. Dynamic List Rendering
function DynamicList({ items }) {
const handleItemClick = useCallback((itemId) => {
console.log('Item clicked:', itemId);
}, []);
return (
<ul>
{items.map(item => (
<MemoizedListItem
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</ul>
);
}
Performance Optimization Patterns
1. Combining with React.memo
const ExpensiveChild = React.memo(function({ config, onClick }) {
// Only re-renders when props change
});
function Parent() {
const config = useMemo(() => ({ /* ... */ }), []);
const onClick = useCallback(() => { /* ... */ }, []);
return <ExpensiveChild config={config} onClick={onClick} />;
}
2. Context Value Optimization
const AppContext = createContext();
function AppProvider({ children }) {
const [state, setState] = useState(initialState);
const contextValue = useMemo(() => ({
state,
update: (newState) => setState(prev => ({ ...prev, ...newState }))
}), [state]);
return (
<AppContext.Provider value={contextValue}>
{children}
</AppContext.Provider>
);
}
3. Debouncing/Trottling Callbacks
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedSearch = useCallback(
debounce(searchTerm => {
api.search(searchTerm);
}, 300),
[] // debounce instance created once
);
useEffect(() => {
debouncedSearch(query);
}, [query, debouncedSearch]);
return <input onChange={(e) => setQuery(e.target.value)} />;
}
Common Pitfalls and Best Practices
When NOT to Use
Overusing useMemo
// ❌ Unnecessary - simple calculations are cheap
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// ✅ Better
const fullName = `${firstName} ${lastName}`;
Empty Dependency Arrays
// ❌ Potentially stale closure
const handleSubmit = useCallback(() => {
submitForm(values); // values might be stale
}, []);
// ✅ Include all dependencies
const handleSubmit = useCallback(() => {
submitForm(values);
}, [values]);
Best Practices
- Profile First: Use React DevTools to identify real performance issues before optimizing
- Dependency Accuracy: Always include all values used in the callback/calculation
- Component Splitting: Often more effective than memoization
- Avoid Premature Optimization: Don’t memoize everything “just in case”
- TypeScript: Helps catch dependency array mistakes
// TypeScript can help catch missing dependencies
const handleSubmit = useCallback((newValue: string) => {
setValues(prev => ({ ...prev, field: newValue }));
}, []); // TypeScript might warn about missing setValues dependency
Advanced Patterns
1. Custom Comparison for React.memo
const Item = React.memo(
function Item({ item }) {
return <div>{item.name}</div>;
},
(prevProps, nextProps) => {
// Custom comparison - only re-render if id changed
return prevProps.item.id === nextProps.item.id;
}
);
2. Memoizing Multiple Values
function useUserSettings(userId) {
const [settings, setSettings] = useState(null);
const [preferences, setPreferences] = useState(null);
return useMemo(() => ({
settings,
preferences,
updateSettings: setSettings,
updatePreferences: setPreferences
}), [settings, preferences]);
}
3. Combining with useReducer
function TodoApp() {
const [state, dispatch] = useReducer(reducer, initialState);
// Stable dispatch reference
const actions = useMemo(() => ({
addTodo: text => dispatch({ type: 'ADD_TODO', text }),
toggleTodo: id => dispatch({ type: 'TOGGLE_TODO', id })
}), []);
return <TodoList todos={state.todos} actions={actions} />;
}
Performance Measurement
function ExpensiveComponent() {
const startTime = performance.now();
// ... component render logic
useEffect(() => {
const endTime = performance.now();
console.log(`Render took ${endTime - startTime}ms`);
});
return (
// ... component JSX
);
}
Remember that useMemo
and useCallback
are optimization tools that come with their own overhead. They should be used judiciously when you have measurable performance problems, not as a default for all computations and functions. Always profile your application to identify real bottlenecks before applying these optimizations.