A concise, code-focused tutorial on optimizing React apps using the useMemo hook to avoid unnecessary computations.
1. Introduction
React’s useMemo hook memoizes expensive calculations, recomputing only when dependencies change. It’s essential for performance in apps with heavy computations, like data processing or complex derivations.
Introduced in React 16.8, useMemo prevents re-runs on every render, saving resources in large components.
We’ll cover the problem it solves, the solution, detailed implementations with a real-life e-commerce filtering scenario, and key takeaways. Heavy on code for quick learning.
2. The Problem
In React, components re-render on state/prop changes, re-executing all code—including expensive functions. This causes performance issues:
- Lagging UIs in data-heavy apps.
- Unnecessary CPU usage for unchanged results.
- Battery drain on mobile.
Example: A component computing Fibonacci on every render:
function SlowComponent({ n }) {
const fib = computeFib(n); // Expensive, runs every render
return <div>Fib({n}): {fib}</div>;
}
function computeFib(n) {
if (n <= 1) return n;
return computeFib(n - 1) + computeFib(n - 2);
}Renders slow for large n, even if n unchanged.
3. The Solution
useMemo memoizes values:
import { useMemo } from 'react';
const memoizedValue = useMemo(() => expensiveFunction(a, b), [a, b]);- Runs callback only if dependencies [a, b] change.
- Returns cached value otherwise.
Best for pure functions with costly ops. Combine with useCallback for memoized callbacks.
Pros: Improves render speed, reduces side effects. Cons: Overuse adds overhead; use profiling to identify needs.
4. Implementation
Let’s use a real-life scenario: An e-commerce product list with filtering and sorting. Without memoization, filtering a large array (e.g., 1000+ products) re-runs on every render, causing jank during typing or state updates.
App structure:
• ProductList: Displays filtered/sorted products.
• Filters: Search input, category select.
• Expensive ops: Filter by search, sort by price.
Step 1: Basic Setup Without useMemo (Slow)
// Products.js - Sample data
export const products = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.random() * 100,
category: ['Electronics', 'Books', 'Clothing'][Math.floor(Math.random() * 3)],
}));// ProductList.js
import { useState } from 'react';
import { products } from './Products';
function ProductList() {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('All');
// Expensive: Filters and sorts every render
const filteredProducts = products
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.filter(p => category === 'All' || p.category === category)
.sort((a, b) => a.price - b.price);
return (
<div>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search products..."
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option>All</option>
<option>Electronics</option>
<option>Books</option>
<option>Clothing</option>
</select>
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name} - ${p.price.toFixed(2)}</li>
))}
</ul>
</div>
);
}
export default ProductList;Issue: Typing in search re-renders, re-filtering all 1000 items unnecessarily if products unchanged.
Step 2: Optimize with useMemo
// Optimized ProductList.js
import { useState, useMemo } from 'react';
import { products } from './Products';
function ProductList() {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('All');
const filteredProducts = useMemo(() => {
console.log('Computing filtered products...'); // Logs only on dep change
return products
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.filter(p => category === 'All' || p.category === category)
.sort((a, b) => a.price - b.price);
}, [search, category]); // Dependencies
return (
<div>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search products..."
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option>All</option>
<option>Electronics</option>
<option>Books</option>
<option>Clothing</option>
</select>
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name} - ${p.price.toFixed(2)}</li>
))}
</ul>
</div>
);
}
export default ProductList;Now, computation runs only when search or category changes. Other re-renders (e.g., parent updates) use cached value.
Example 2: Memoizing Complex Objects
const stats = useMemo(() => ({
count: filteredProducts.length,
avgPrice: filteredProducts.reduce((sum, p) => sum + p.price, 0) / filteredProducts.length || 0,
}), [filteredProducts]);Add to return: <p>Items: {stats.count}, Avg Price: ${stats.avgPrice.toFixed(2)}</p>
Example 3: With useCallback for Memoized Functions
import { memo } from 'react';
// Child component
const ProductItem = memo(({ product, onClick }) => {
return <li onClick={() => onClick(product.id)}>{product.name} - ${product.price.toFixed(2)}</li>;
});
// In ProductList
const handleClick = useCallback((id) => {
console.log(`Clicked ${id}`);
}, []); // Empty deps if no dependencies
// In map: <ProductItem key={p.id} product={p} onClick={handleClick} />useMemo for values, useCallback for functions—prevents child re-renders.
Real-Life Tips
- Use React DevTools Profiler to spot slow renders.
- Avoid large dependency arrays; optimize upstream.
- For API data: Memoize after fetching.
- Test with large datasets (e.g., 10k items) to see gains.
This e-commerce example shows useMemo cutting render times by 80%+ in benchmarks.
5. Conclusion
useMemo is key for performant React apps, caching computations to skip unnecessary work.
From basic filtering to complex derivations, it shines in data-intensive UIs like dashboards or shops.
Prioritize it for expensive pure functions. Combine with memo, useCallback for full optimization.