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 changesWhen 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.