React components re-render frequently, and every time they do, all code inside runs again. If you have expensive calculations (complex math, data processing, filtering large lists), they'll run on every render - even if their inputs haven't changed! The useMemo hook solves this by memoizing (caching) calculation results, recomputing only when necessary. Let's learn how to optimize your React apps! ⚡
The Problem: Expensive Re-computations
Without useMemo, expensive operations run on every render:
function ProductList({ products }) {
const [sortOrder, setSortOrder] = React.useState('asc')
const [filterText, setFilterText] = React.useState('')
// ❌ Problem: This runs on EVERY render!
const expensiveCalculation = () => {
console.log('Computing expensive result...')
let result = 0
for (let i = 0; i < 1000000000; i++) {
result += i
}
return result
}
const expensive = expensiveCalculation() // Runs every render!
return (
<div>
<p>Result: {expensive}</p>
<input
value={filterText}
onChange={e => setFilterText(e.target.value)}
placeholder="Filter..."
/>
<button onClick={() => setSortOrder('desc')}>Sort</button>
</div>
)
}
// Problem:
// 1. User types in input
// 2. filterText changes, component re-renders
// 3. expensiveCalculation runs again (even though result is same!)
// 4. App feels slow and laggyWhat is Memoization?
Memoization is caching a function's result based on its inputs.
If the inputs haven't changed, return the cached result instead of recomputing.
It's like remembering the answer to a math problem instead of solving it again!
The Solution: useMemo
useMemo caches the result and only recomputes when dependencies change:
function ProductList({ products }) {
const [sortOrder, setSortOrder] = React.useState('asc')
const [filterText, setFilterText] = React.useState('')
// ✅ Solution: Memoize the expensive calculation
const expensive = React.useMemo(() => {
console.log('Computing expensive result...')
let result = 0
for (let i = 0; i < 1000000000; i++) {
result += i
}
return result
}, []) // Empty array = compute once, cache forever
return (
<div>
<p>Result: {expensive}</p>
<input
value={filterText}
onChange={e => setFilterText(e.target.value)}
placeholder="Filter..."
/>
<button onClick={() => setSortOrder('desc')}>Sort</button>
</div>
)
}
// How it works:
// 1. First render: Computation runs, result cached
// 2. User types in input, component re-renders
// 3. useMemo returns cached result (no recomputation!)
// 4. App stays fast and responsiveuseMemo Syntax
const memoizedValue = React.useMemo(
() => {
// Expensive calculation
return result
},
[dependencies] // Recompute when these change
)
// Parts:
// 1. Callback function - Returns the value to memoize
// 2. Dependency array - When to recompute
// Example with dependencies:
const filtered = React.useMemo(() => {
return products.filter(p => p.name.includes(searchTerm))
}, [products, searchTerm]) // Recompute when products or searchTerm change🤔 useMemo vs useEffect
useMemo: Returns a memoized VALUEconst value = useMemo(() => calculation, [deps])
useEffect: Runs side effects, returns nothing (or cleanup)useEffect(() => { doSideEffect() }, [deps])
Use useMemo for calculations, useEffect for side effects!
Common useMemo Use Cases
1. Filtering Large Lists
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = React.useState('')
const [category, setCategory] = React.useState('all')
// Memoize filtered products
const filteredProducts = React.useMemo(() => {
console.log('Filtering products...')
return products
.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
)
.filter(p =>
category === 'all' || p.category === category
)
}, [products, searchTerm, category])
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)
}
// Without useMemo: Filters every render (slow for large lists)
// With useMemo: Filters only when products, searchTerm, or category change2. Sorting Data
function UserTable({ users }) {
const [sortBy, setSortBy] = React.useState('name')
const [sortOrder, setSortOrder] = React.useState('asc')
// Memoize sorted users
const sortedUsers = React.useMemo(() => {
console.log('Sorting users...')
const sorted = [...users].sort((a, b) => {
if (a[sortBy] < b[sortBy]) return sortOrder === 'asc' ? -1 : 1
if (a[sortBy] > b[sortBy]) return sortOrder === 'asc' ? 1 : -1
return 0
})
return sorted
}, [users, sortBy, sortOrder])
return (
<table>
<thead>
<tr>
<th onClick={() => setSortBy('name')}>Name</th>
<th onClick={() => setSortBy('age')}>Age</th>
<th onClick={() => setSortBy('email')}>Email</th>
</tr>
</thead>
<tbody>
{sortedUsers.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
)
}3. Complex Calculations
function DataDashboard({ data }) {
const [showDetails, setShowDetails] = React.useState(false)
// Memoize statistics calculation
const stats = React.useMemo(() => {
console.log('Computing statistics...')
return {
total: data.reduce((sum, item) => sum + item.value, 0),
average: data.reduce((sum, item) => sum + item.value, 0) / data.length,
max: Math.max(...data.map(item => item.value)),
min: Math.min(...data.map(item => item.value)),
median: calculateMedian(data.map(item => item.value))
}
}, [data]) // Only recompute when data changes
return (
<div>
<h2>Statistics</h2>
<p>Total: {stats.total}</p>
<p>Average: {stats.average}</p>
<p>Max: {stats.max}</p>
<p>Min: {stats.min}</p>
<p>Median: {stats.median}</p>
<button onClick={() => setShowDetails(!showDetails)}>
Toggle Details
</button>
{/* Toggling showDetails doesn't recompute stats! */}
</div>
)
}4. Preventing Reference Changes
function Parent() {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('')
// ❌ Without useMemo: New object every render
const config = { theme: 'dark', language: 'en' }
// ✅ With useMemo: Same object reference
const config = React.useMemo(() => ({
theme: 'dark',
language: 'en'
}), []) // Object recreated only on mount
return <Child config={config} />
}
// Why this matters:
// If Child is wrapped in React.memo, it won't re-render
// unnecessarily when config reference stays the sameWhen to Use useMemo
✅ DO Use useMemo When:
- Expensive calculations - Complex math, data processing
- Filtering/sorting large lists - Arrays with 1000+ items
- Complex transformations - Mapping, reducing large datasets
- Preventing reference changes - Objects/arrays passed to React.memo components
- Measured performance issue - You've profiled and found a bottleneck
❌ DON'T Use useMemo When:
- Simple calculations - Adding numbers, simple string operations
- Small lists - Arrays with <100 items
- No performance problem - Don't optimize prematurely!
- Everywhere "just in case" - useMemo has overhead too
Premature Optimization Warning:
Don't use useMemo everywhere! It adds complexity and has its own overhead.
Steps to optimize:
1. Write code normally
2. Measure performance (React DevTools Profiler)
3. If you find a real problem, then add useMemo
"Premature optimization is the root of all evil" - Donald Knuth
Common useMemo Mistakes
1. Missing Dependencies
// ❌ Wrong - Missing searchTerm dependency
const filtered = React.useMemo(() => {
return products.filter(p => p.name.includes(searchTerm))
}, [products]) // Missing searchTerm!
// Result: Stale data when searchTerm changes
// ✅ Correct - Include all dependencies
const filtered = React.useMemo(() => {
return products.filter(p => p.name.includes(searchTerm))
}, [products, searchTerm])2. Overusing useMemo
// ❌ Bad - Overusing for simple operations
function Component({ a, b }) {
const sum = React.useMemo(() => a + b, [a, b]) // Unnecessary!
const double = React.useMemo(() => sum * 2, [sum]) // Unnecessary!
return <div>{double}</div>
}
// ✅ Good - No memoization needed
function Component({ a, b }) {
const sum = a + b // Fast, no need to memoize
const double = sum * 2 // Fast, no need to memoize
return <div>{double}</div>
}
// Rule: Only memoize if calculation is actually expensive3. Memoizing Components Instead of Values
// ❌ Wrong - useMemo for components
const MyComponent = React.useMemo(() => {
return <div>Component</div>
}, [])
// ✅ Correct - Use React.memo for components
const MyComponent = React.memo(function MyComponent() {
return <div>Component</div>
})
// useMemo = for values
// React.memo = for components4. Complex Dependency Arrays
// ❌ Problematic - Object in dependencies
const result = React.useMemo(() => {
return calculate(config)
}, [config]) // config is object, new reference every render!
// ✅ Better - Destructure or use specific properties
const result = React.useMemo(() => {
return calculate(config)
}, [config.setting1, config.setting2])
// ✅ Or memoize config too
const config = React.useMemo(() => ({
setting1: value1,
setting2: value2
}), [value1, value2])useMemo Patterns
Pattern 1: Expensive List Operations
function DataTable({ data, filters }) {
// Chain memoized operations
const filtered = React.useMemo(() => {
return data.filter(item =>
filters.category === 'all' || item.category === filters.category
)
}, [data, filters.category])
const sorted = React.useMemo(() => {
return [...filtered].sort((a, b) =>
a[filters.sortBy] > b[filters.sortBy] ? 1 : -1
)
}, [filtered, filters.sortBy])
const paginated = React.useMemo(() => {
const start = (filters.page - 1) * filters.pageSize
return sorted.slice(start, start + filters.pageSize)
}, [sorted, filters.page, filters.pageSize])
return (
<table>
{paginated.map(item => <Row key={item.id} item={item} />)}
</table>
)
}Pattern 2: Memoized Context Value
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState('light')
// Memoize context value to prevent unnecessary re-renders
const value = React.useMemo(() => ({
theme,
setTheme,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
}), [theme])
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}Pattern 3: Derived State
function ShoppingCart({ items }) {
// Memoize calculations based on items
const summary = React.useMemo(() => {
return {
subtotal: items.reduce((sum, item) => sum + item.price * item.quantity, 0),
tax: items.reduce((sum, item) => sum + item.price * item.quantity, 0) * 0.1,
shipping: items.length > 0 ? 5 : 0,
get total() { return this.subtotal + this.tax + this.shipping }
}
}, [items])
return (
<div>
<p>Subtotal: ${summary.subtotal}</p>
<p>Tax: ${summary.tax}</p>
<p>Shipping: ${summary.shipping}</p>
<p>Total: ${summary.total}</p>
</div>
)
}Key Takeaways
useMemocaches calculated values- Only recomputes when dependencies change
- Use for expensive calculations and large list operations
- Helps prevent unnecessary re-computations
- Returns a memoized VALUE (not a function)
- Include all dependencies in the array
- Don't use for simple calculations (overhead not worth it)
- Measure before optimizing - avoid premature optimization
- Can memoize objects/arrays to prevent reference changes
- Useful for optimizing context values
- Different from useCallback (which memoizes functions)
- Use React DevTools Profiler to find real bottlenecks
- Memoization trades memory for speed
What's Next?
You now understand useMemo and how to optimize expensive calculations! You know when to use memoization and when it's unnecessary.
In the next lesson, you'll learn about useCallback Hook:
- What is useCallback and how it differs from useMemo
- Memoizing functions to prevent re-renders
- When to use useCallback vs useMemo
- Optimizing callback props
- Common useCallback patterns
Get ready to complete your performance optimization toolkit! 🚀