Back to Blog
ReactHooksuseMemoPerformance

React useMemo Guide

Master React useMemo hook for optimizing expensive calculations and preventing unnecessary re-renders.

B
Bootspring Team
Engineering
August 16, 2018
7 min read

useMemo memoizes expensive calculations, recomputing only when dependencies change. Here's how to use it effectively.

Basic Usage#

1import { useMemo, useState } from 'react'; 2 3function ExpensiveComponent({ items, filter }) { 4 // Memoize expensive filtering 5 const filteredItems = useMemo(() => { 6 console.log('Filtering items...'); 7 return items.filter(item => 8 item.name.toLowerCase().includes(filter.toLowerCase()) 9 ); 10 }, [items, filter]); 11 12 return ( 13 <ul> 14 {filteredItems.map(item => ( 15 <li key={item.id}>{item.name}</li> 16 ))} 17 </ul> 18 ); 19} 20 21// Without useMemo, filtering runs on every render 22// With useMemo, it only runs when items or filter changes

When to Use useMemo#

1// 1. Expensive calculations 2function DataAnalytics({ data }) { 3 const statistics = useMemo(() => { 4 // Complex statistical calculations 5 const sum = data.reduce((a, b) => a + b, 0); 6 const mean = sum / data.length; 7 const variance = data.reduce((acc, val) => 8 acc + Math.pow(val - mean, 2), 0 9 ) / data.length; 10 const stdDev = Math.sqrt(variance); 11 12 return { sum, mean, variance, stdDev }; 13 }, [data]); 14 15 return <Stats data={statistics} />; 16} 17 18// 2. Referential equality for objects/arrays 19function Parent({ items }) { 20 // Without useMemo: new array every render 21 // const sortedItems = [...items].sort(); 22 23 // With useMemo: same reference if items unchanged 24 const sortedItems = useMemo(() => 25 [...items].sort((a, b) => a.name.localeCompare(b.name)), 26 [items] 27 ); 28 29 // Child won't re-render unnecessarily 30 return <MemoizedChild items={sortedItems} />; 31} 32 33// 3. Derived state 34function UserProfile({ user, preferences }) { 35 const displayConfig = useMemo(() => ({ 36 fullName: `${user.firstName} ${user.lastName}`, 37 theme: preferences.darkMode ? 'dark' : 'light', 38 locale: preferences.language || 'en', 39 avatar: user.avatar || generateAvatar(user.name) 40 }), [user, preferences]); 41 42 return <Profile config={displayConfig} />; 43}

Dependencies#

1// All values used inside must be in dependencies 2function SearchResults({ query, sortBy, filters }) { 3 const results = useMemo(() => { 4 let data = searchDatabase(query); 5 6 // Apply filters 7 if (filters.category) { 8 data = data.filter(item => item.category === filters.category); 9 } 10 11 // Sort results 12 return data.sort((a, b) => { 13 if (sortBy === 'name') return a.name.localeCompare(b.name); 14 if (sortBy === 'date') return new Date(b.date) - new Date(a.date); 15 return 0; 16 }); 17 }, [query, sortBy, filters]); // All dependencies listed 18 19 return <ResultsList results={results} />; 20} 21 22// Object dependencies - be careful! 23function Component({ config }) { 24 // This recalculates every render if config is new object each time 25 const processed = useMemo(() => process(config), [config]); 26 27 // Better: depend on specific properties 28 const processed2 = useMemo(() => 29 process(config.value, config.options), 30 [config.value, config.options] 31 ); 32}

useMemo vs useCallback#

1// useMemo: memoizes a VALUE 2const memoizedValue = useMemo(() => computeValue(a, b), [a, b]); 3 4// useCallback: memoizes a FUNCTION 5const memoizedFn = useCallback(() => doSomething(a, b), [a, b]); 6 7// useCallback is equivalent to: 8const memoizedFn2 = useMemo(() => () => doSomething(a, b), [a, b]); 9 10// When to use which: 11function Parent({ items }) { 12 // useMemo for computed values 13 const total = useMemo(() => 14 items.reduce((sum, item) => sum + item.price, 0), 15 [items] 16 ); 17 18 // useCallback for event handlers passed to children 19 const handleClick = useCallback((id) => { 20 console.log('Clicked:', id); 21 }, []); 22 23 return ( 24 <div> 25 <Total value={total} /> 26 <ItemList items={items} onItemClick={handleClick} /> 27 </div> 28 ); 29}

Referential Equality#

1import { useMemo, memo } from 'react'; 2 3// Memoized child component 4const ExpensiveChild = memo(function ExpensiveChild({ data, config }) { 5 console.log('ExpensiveChild rendered'); 6 return <div>{/* expensive rendering */}</div>; 7}); 8 9function Parent({ items }) { 10 const [count, setCount] = useState(0); 11 12 // Without useMemo: new object every render 13 // ExpensiveChild re-renders even when only count changes 14 // const config = { sortBy: 'name', order: 'asc' }; 15 16 // With useMemo: same reference, child doesn't re-render 17 const config = useMemo(() => ({ 18 sortBy: 'name', 19 order: 'asc' 20 }), []); 21 22 const processedItems = useMemo(() => 23 items.map(item => ({ ...item, processed: true })), 24 [items] 25 ); 26 27 return ( 28 <div> 29 <button onClick={() => setCount(c => c + 1)}> 30 Count: {count} 31 </button> 32 <ExpensiveChild data={processedItems} config={config} /> 33 </div> 34 ); 35}

Conditional Memoization#

1function DataTable({ data, viewMode }) { 2 // Different calculations based on mode 3 const processedData = useMemo(() => { 4 if (viewMode === 'chart') { 5 return data.map(item => ({ 6 x: item.date, 7 y: item.value 8 })); 9 } 10 11 if (viewMode === 'summary') { 12 return { 13 total: data.reduce((sum, item) => sum + item.value, 0), 14 average: data.reduce((sum, item) => sum + item.value, 0) / data.length, 15 count: data.length 16 }; 17 } 18 19 return data; 20 }, [data, viewMode]); 21 22 return viewMode === 'chart' 23 ? <Chart data={processedData} /> 24 : <Table data={processedData} />; 25}

Lazy Initialization#

1// useMemo for expensive initial values 2function HeavyComponent({ initialData }) { 3 // This runs on every render without useMemo 4 // const parsed = JSON.parse(initialData); 5 6 // Only parse once (or when initialData changes) 7 const parsed = useMemo(() => { 8 console.log('Parsing JSON...'); 9 return JSON.parse(initialData); 10 }, [initialData]); 11 12 // For truly one-time initialization, consider useState 13 const [data] = useState(() => JSON.parse(initialData)); 14 15 return <Display data={parsed} />; 16} 17 18// Complex object initialization 19function FormBuilder({ schema }) { 20 const formConfig = useMemo(() => { 21 // Build form configuration from schema 22 return schema.fields.map(field => ({ 23 ...field, 24 validators: buildValidators(field.validation), 25 formatter: buildFormatter(field.format), 26 component: getFieldComponent(field.type) 27 })); 28 }, [schema]); 29 30 return <DynamicForm config={formConfig} />; 31}

Common Patterns#

1// Sorting and filtering 2function ProductList({ products, sortBy, filterBy }) { 3 const displayProducts = useMemo(() => { 4 let result = [...products]; 5 6 // Filter 7 if (filterBy) { 8 result = result.filter(p => p.category === filterBy); 9 } 10 11 // Sort 12 result.sort((a, b) => { 13 switch (sortBy) { 14 case 'price-asc': return a.price - b.price; 15 case 'price-desc': return b.price - a.price; 16 case 'name': return a.name.localeCompare(b.name); 17 default: return 0; 18 } 19 }); 20 21 return result; 22 }, [products, sortBy, filterBy]); 23 24 return <Grid products={displayProducts} />; 25} 26 27// Formatted display values 28function PriceDisplay({ amount, currency, locale }) { 29 const formatted = useMemo(() => { 30 return new Intl.NumberFormat(locale, { 31 style: 'currency', 32 currency: currency 33 }).format(amount); 34 }, [amount, currency, locale]); 35 36 return <span>{formatted}</span>; 37} 38 39// Context value 40function ThemeProvider({ children }) { 41 const [theme, setTheme] = useState('light'); 42 43 // Memoize context value to prevent consumer re-renders 44 const value = useMemo(() => ({ 45 theme, 46 setTheme, 47 isDark: theme === 'dark' 48 }), [theme]); 49 50 return ( 51 <ThemeContext.Provider value={value}> 52 {children} 53 </ThemeContext.Provider> 54 ); 55}

When NOT to Use useMemo#

1// DON'T: Simple calculations 2function Component({ a, b }) { 3 // Unnecessary - addition is fast 4 const sum = useMemo(() => a + b, [a, b]); 5 6 // Just do it directly 7 const sum2 = a + b; 8} 9 10// DON'T: Primitive values 11function Component({ name }) { 12 // Strings are already compared by value 13 const greeting = useMemo(() => `Hello, ${name}`, [name]); 14 15 // Just use directly 16 const greeting2 = `Hello, ${name}`; 17} 18 19// DON'T: When memoization cost exceeds benefit 20function Component({ items }) { 21 // If items rarely changes and computation is simple 22 const count = useMemo(() => items.length, [items]); 23 24 // Just access directly 25 const count2 = items.length; 26} 27 28// DON'T: Non-deterministic values 29function Component() { 30 // Random values shouldn't be memoized 31 const random = useMemo(() => Math.random(), []); 32 // Will always return same value! 33}

Debugging useMemo#

1// Add logging to track recalculations 2function Component({ data, filter }) { 3 const filtered = useMemo(() => { 4 console.log('useMemo recalculating:', { data, filter }); 5 return data.filter(item => item.includes(filter)); 6 }, [data, filter]); 7 8 // Use React DevTools Profiler to see render causes 9 10 return <List items={filtered} />; 11} 12 13// Check if dependencies are stable 14function Component({ config }) { 15 // Log to see if config changes 16 useEffect(() => { 17 console.log('Config changed:', config); 18 }, [config]); 19 20 const processed = useMemo(() => process(config), [config]); 21}

Best Practices#

Use useMemo When: ✓ Expensive calculations ✓ Referential equality matters ✓ Preventing child re-renders ✓ Deriving complex state Don't Use When: ✗ Simple calculations ✗ Primitive values ✗ Computation is cheap ✗ Value isn't used in renders Dependencies: ✓ Include all values used inside ✓ Use specific properties, not objects ✓ Consider dependency stability ✓ Use ESLint exhaustive-deps rule Performance: ✓ Profile before optimizing ✓ Measure actual impact ✓ Consider component structure ✓ Don't over-optimize

Conclusion#

useMemo optimizes performance by memoizing expensive calculations and maintaining referential equality. Use it for computationally heavy operations, when passing objects/arrays to memoized children, or when deriving complex state. Always include all dependencies and profile your app to ensure memoization actually helps. Remember: premature optimization is the root of all evil - measure first, optimize second.

Share this article

Help spread the word about Bootspring