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.