Back to Blog
ReactHooksuseMemoPerformance

React useMemo Patterns Guide

Master React useMemo for optimizing expensive computations and preventing unnecessary recalculations.

B
Bootspring Team
Engineering
August 7, 2019
7 min read

The useMemo hook memoizes expensive computations to avoid recalculating on every render. Here's when and how to use it effectively.

Basic Usage#

1import { useMemo, useState } from 'react'; 2 3function ExpensiveComponent({ items }: { items: number[] }) { 4 const [filter, setFilter] = useState(''); 5 6 // Memoized - only recalculates when items changes 7 const sortedItems = useMemo(() => { 8 console.log('Sorting items...'); 9 return [...items].sort((a, b) => a - b); 10 }, [items]); 11 12 // NOT memoized - recalculates every render 13 const total = items.reduce((sum, item) => sum + item, 0); 14 15 return ( 16 <div> 17 <input value={filter} onChange={(e) => setFilter(e.target.value)} /> 18 <ul> 19 {sortedItems.map((item) => ( 20 <li key={item}>{item}</li> 21 ))} 22 </ul> 23 </div> 24 ); 25}

Expensive Calculations#

1import { useMemo, useState } from 'react'; 2 3function DataAnalytics({ data }: { data: DataPoint[] }) { 4 const [selectedMetric, setSelectedMetric] = useState('revenue'); 5 6 // Expensive aggregation 7 const statistics = useMemo(() => { 8 console.log('Computing statistics...'); 9 10 const values = data.map((d) => d[selectedMetric]); 11 12 return { 13 sum: values.reduce((a, b) => a + b, 0), 14 average: values.reduce((a, b) => a + b, 0) / values.length, 15 min: Math.min(...values), 16 max: Math.max(...values), 17 median: calculateMedian(values), 18 standardDeviation: calculateStdDev(values), 19 }; 20 }, [data, selectedMetric]); 21 22 return ( 23 <div> 24 <select 25 value={selectedMetric} 26 onChange={(e) => setSelectedMetric(e.target.value)} 27 > 28 <option value="revenue">Revenue</option> 29 <option value="users">Users</option> 30 <option value="orders">Orders</option> 31 </select> 32 <div> 33 <p>Sum: {statistics.sum}</p> 34 <p>Average: {statistics.average.toFixed(2)}</p> 35 <p>Min: {statistics.min}</p> 36 <p>Max: {statistics.max}</p> 37 </div> 38 </div> 39 ); 40}

Filtering and Sorting#

1import { useMemo, useState } from 'react'; 2 3interface User { 4 id: string; 5 name: string; 6 email: string; 7 role: string; 8 createdAt: Date; 9} 10 11function UserList({ users }: { users: User[] }) { 12 const [search, setSearch] = useState(''); 13 const [sortBy, setSortBy] = useState<keyof User>('name'); 14 const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); 15 const [roleFilter, setRoleFilter] = useState<string>('all'); 16 17 // Chain filtering and sorting 18 const filteredAndSortedUsers = useMemo(() => { 19 let result = [...users]; 20 21 // Filter by search 22 if (search) { 23 const searchLower = search.toLowerCase(); 24 result = result.filter( 25 (user) => 26 user.name.toLowerCase().includes(searchLower) || 27 user.email.toLowerCase().includes(searchLower) 28 ); 29 } 30 31 // Filter by role 32 if (roleFilter !== 'all') { 33 result = result.filter((user) => user.role === roleFilter); 34 } 35 36 // Sort 37 result.sort((a, b) => { 38 const aVal = a[sortBy]; 39 const bVal = b[sortBy]; 40 41 if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1; 42 if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1; 43 return 0; 44 }); 45 46 return result; 47 }, [users, search, sortBy, sortOrder, roleFilter]); 48 49 return ( 50 <div> 51 <input 52 placeholder="Search..." 53 value={search} 54 onChange={(e) => setSearch(e.target.value)} 55 /> 56 <p>Showing {filteredAndSortedUsers.length} users</p> 57 {filteredAndSortedUsers.map((user) => ( 58 <UserCard key={user.id} user={user} /> 59 ))} 60 </div> 61 ); 62}

Derived State#

1import { useMemo, useState } from 'react'; 2 3interface CartItem { 4 id: string; 5 name: string; 6 price: number; 7 quantity: number; 8} 9 10function ShoppingCart({ items }: { items: CartItem[] }) { 11 const [couponCode, setCouponCode] = useState(''); 12 13 // Derive multiple values from items 14 const cartSummary = useMemo(() => { 15 const subtotal = items.reduce( 16 (sum, item) => sum + item.price * item.quantity, 17 0 18 ); 19 20 const itemCount = items.reduce((sum, item) => sum + item.quantity, 0); 21 22 const discount = couponCode === 'SAVE20' ? subtotal * 0.2 : 0; 23 24 const tax = (subtotal - discount) * 0.08; 25 26 const total = subtotal - discount + tax; 27 28 return { 29 subtotal, 30 itemCount, 31 discount, 32 tax, 33 total, 34 }; 35 }, [items, couponCode]); 36 37 return ( 38 <div> 39 <p>Items: {cartSummary.itemCount}</p> 40 <p>Subtotal: ${cartSummary.subtotal.toFixed(2)}</p> 41 {cartSummary.discount > 0 && ( 42 <p>Discount: -${cartSummary.discount.toFixed(2)}</p> 43 )} 44 <p>Tax: ${cartSummary.tax.toFixed(2)}</p> 45 <p>Total: ${cartSummary.total.toFixed(2)}</p> 46 </div> 47 ); 48}

Object/Array Stability#

1import { useMemo, memo } from 'react'; 2 3// Child component with memo 4const ExpensiveChild = memo(function ExpensiveChild({ 5 config, 6 items, 7}: { 8 config: { theme: string; size: string }; 9 items: string[]; 10}) { 11 console.log('ExpensiveChild rendered'); 12 return <div>{/* ... */}</div>; 13}); 14 15function Parent({ theme, size }: { theme: string; size: string }) { 16 const [count, setCount] = useState(0); 17 18 // Without useMemo: new object every render, child re-renders 19 // const config = { theme, size }; 20 21 // With useMemo: stable reference, child doesn't re-render unnecessarily 22 const config = useMemo(() => ({ theme, size }), [theme, size]); 23 24 // Same for arrays 25 const items = useMemo(() => ['a', 'b', 'c'], []); 26 27 return ( 28 <div> 29 <button onClick={() => setCount((c) => c + 1)}> 30 Count: {count} 31 </button> 32 <ExpensiveChild config={config} items={items} /> 33 </div> 34 ); 35}

Complex Data Transformations#

1import { useMemo } from 'react'; 2 3interface RawData { 4 timestamp: string; 5 value: number; 6 category: string; 7} 8 9interface ChartData { 10 labels: string[]; 11 datasets: { 12 label: string; 13 data: number[]; 14 }[]; 15} 16 17function Chart({ rawData }: { rawData: RawData[] }) { 18 // Transform raw data to chart format 19 const chartData = useMemo<ChartData>(() => { 20 // Group by category 21 const grouped = rawData.reduce((acc, item) => { 22 if (!acc[item.category]) { 23 acc[item.category] = []; 24 } 25 acc[item.category].push(item); 26 return acc; 27 }, {} as Record<string, RawData[]>); 28 29 // Get unique timestamps for labels 30 const labels = [...new Set(rawData.map((d) => d.timestamp))].sort(); 31 32 // Create datasets 33 const datasets = Object.entries(grouped).map(([category, items]) => ({ 34 label: category, 35 data: labels.map((label) => { 36 const item = items.find((i) => i.timestamp === label); 37 return item?.value ?? 0; 38 }), 39 })); 40 41 return { labels, datasets }; 42 }, [rawData]); 43 44 return <ChartComponent data={chartData} />; 45}

Search Index#

1import { useMemo, useState } from 'react'; 2 3interface Document { 4 id: string; 5 title: string; 6 content: string; 7 tags: string[]; 8} 9 10function SearchableDocuments({ documents }: { documents: Document[] }) { 11 const [query, setQuery] = useState(''); 12 13 // Build search index once 14 const searchIndex = useMemo(() => { 15 console.log('Building search index...'); 16 17 return documents.map((doc) => ({ 18 id: doc.id, 19 searchText: [ 20 doc.title.toLowerCase(), 21 doc.content.toLowerCase(), 22 ...doc.tags.map((t) => t.toLowerCase()), 23 ].join(' '), 24 document: doc, 25 })); 26 }, [documents]); 27 28 // Search is fast because index is ready 29 const results = useMemo(() => { 30 if (!query) return documents; 31 32 const queryLower = query.toLowerCase(); 33 return searchIndex 34 .filter((item) => item.searchText.includes(queryLower)) 35 .map((item) => item.document); 36 }, [searchIndex, query]); 37 38 return ( 39 <div> 40 <input 41 value={query} 42 onChange={(e) => setQuery(e.target.value)} 43 placeholder="Search documents..." 44 /> 45 <ul> 46 {results.map((doc) => ( 47 <li key={doc.id}>{doc.title}</li> 48 ))} 49 </ul> 50 </div> 51 ); 52}

Context Value Memoization#

1import { createContext, useContext, useMemo, useState } from 'react'; 2 3interface ThemeContextValue { 4 theme: 'light' | 'dark'; 5 toggleTheme: () => void; 6 colors: { 7 primary: string; 8 secondary: string; 9 background: string; 10 }; 11} 12 13const ThemeContext = createContext<ThemeContextValue | null>(null); 14 15function ThemeProvider({ children }: { children: React.ReactNode }) { 16 const [theme, setTheme] = useState<'light' | 'dark'>('light'); 17 18 // Memoize entire context value 19 const value = useMemo<ThemeContextValue>(() => ({ 20 theme, 21 toggleTheme: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')), 22 colors: theme === 'light' 23 ? { primary: '#3b82f6', secondary: '#6b7280', background: '#ffffff' } 24 : { primary: '#60a5fa', secondary: '#9ca3af', background: '#1f2937' }, 25 }), [theme]); 26 27 return ( 28 <ThemeContext.Provider value={value}> 29 {children} 30 </ThemeContext.Provider> 31 ); 32}

Lazy Computation#

1import { useMemo, useState } from 'react'; 2 3function LazyComponent({ enabled }: { enabled: boolean }) { 4 const [data, setData] = useState<Data | null>(null); 5 6 // Only compute when enabled AND data exists 7 const processedData = useMemo(() => { 8 if (!enabled || !data) { 9 return null; 10 } 11 12 console.log('Processing data...'); 13 return expensiveProcess(data); 14 }, [enabled, data]); 15 16 return ( 17 <div> 18 {processedData && <Results data={processedData} />} 19 </div> 20 ); 21}

When NOT to Use useMemo#

1// ❌ Don't memoize cheap operations 2function BadExample({ name }: { name: string }) { 3 // This is overkill - string concatenation is cheap 4 const greeting = useMemo(() => `Hello, ${name}!`, [name]); 5 6 return <p>{greeting}</p>; 7} 8 9// ❌ Don't memoize when value changes every render 10function AnotherBadExample() { 11 // Dependencies change every render, useMemo is useless 12 const now = useMemo(() => new Date(), [Math.random()]); 13 14 return <p>{now.toString()}</p>; 15} 16 17// ✅ Just compute directly for cheap operations 18function GoodExample({ name }: { name: string }) { 19 const greeting = `Hello, ${name}!`; 20 21 return <p>{greeting}</p>; 22}

Best Practices#

When to Use: ✓ Expensive calculations ✓ Complex data transformations ✓ Stable object/array references ✓ Context provider values Indicators: ✓ Sorting/filtering large arrays ✓ Recursive calculations ✓ Parsing/formatting operations ✓ Building derived data structures Performance: ✓ Profile before optimizing ✓ Keep dependency arrays minimal ✓ Consider computation cost vs memory ✓ Test with realistic data sizes Avoid: ✗ Trivial calculations ✗ Primitive values ✗ Constantly changing dependencies ✗ Premature optimization

Conclusion#

useMemo memoizes expensive computations to avoid recalculating on every render. Use it for expensive operations like sorting large arrays, complex data transformations, or creating stable object references for memoized children. Avoid using it for cheap calculations where the memoization overhead exceeds the computation cost. Always profile first and optimize based on actual performance needs.

Share this article

Help spread the word about Bootspring