Master React performance optimization with useCallback and useMemo hooks. Learn advanced memoization strategies, prevent unnecessary re-renders, and build highly optimized applications.
Performance optimization is crucial for building smooth, responsive React applications. While React is fast by default, complex applications with deep component trees, frequent state updates, and expensive computations can benefit significantly from strategic optimization.
useCallback and useMemo are React's primary optimization hooks, allowing you to memoize functions and values to prevent unnecessary recalculations and re-renders. However, optimization is a double-edged sword—improper use can actually hurt performance by adding overhead without benefits.
In this comprehensive lesson, you'll learn when and how to apply these hooks effectively, understand the performance trade-offs, and build sophisticated optimization strategies that scale with your application's complexity.
Before diving into optimization hooks, it's crucial to understand how React's rendering works and what causes performance issues.
React Rendering Process:
1// Component re-renders occur when:
2// 1. State changes (useState, useReducer)
3// 2. Props change (parent re-renders)
4// 3. Context value changes
5// 4. Parent component re-renders (by default)
6
7function ParentComponent() {
8 const [count, setCount] = useState(0);
9
10 // This object is recreated on every render
11 const expensiveConfig = {
12 theme: 'dark',
13 animations: true,
14 computedValue: someExpensiveCalculation()
15 };
16
17 // This function is recreated on every render
18 const handleClick = () => {
19 setCount(count + 1);
20 };
21
22 return (
23 <div>
24 <ChildComponent
25 config={expensiveConfig} // New object reference every time
26 onClick={handleClick} // New function reference every time
27 />
28 </div>
29 );
30}
31
32// ChildComponent re-renders even if the actual data hasn't changed
33// because it receives new object/function references
34function ChildComponent({ config, onClick }) {
35 console.log('ChildComponent rendered'); // Logs on every parent update
36
37 return (
38 <button onClick={onClick}>
39 Count in {config.theme} theme
40 </button>
41 );
42}Performance Problems:
1. Unnecessary Re-renders:
1function ExpensiveChild({ data, onUpdate }) {
2 // Expensive computation runs on every render
3 const processedData = data.map(item => ({
4 ...item,
5 processed: heavyCalculation(item)
6 }));
7
8 return (
9 <div>
10 {processedData.map(item =>
11 <ExpensiveItem key={item.id} item={item} onUpdate={onUpdate} />
12 )}
13 </div>
14 );
15}2. Reference Equality Issues:
1// These will always be different on each render
2const config = { setting: 'value' }; // New object
3const handler = () => doSomething(); // New function
4const array = [1, 2, 3]; // New array
5
6// Causing child components to re-render unnecessarily
7useEffect(() => {
8 // Runs on every render because dependency changes
9}, [config, handler, array]);3. Expensive Computations:
1function SearchResults({ query, data }) {
2 // Runs expensive filter on every render, even if query hasn't changed
3 const filteredResults = data.filter(item =>
4 item.title.toLowerCase().includes(query.toLowerCase()) &&
5 complexMatchingLogic(item, query)
6 );
7
8 return (
9 <div>
10 {filteredResults.map(result =>
11 <ResultItem key={result.id} result={result} />
12 )}
13 </div>
14 );
15}Performance Measurement:
1// Using React DevTools Profiler
2import { Profiler } from 'react';
3
4function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
5 console.log('Component:', id);
6 console.log('Phase:', phase); // 'mount' or 'update'
7 console.log('Actual duration:', actualDuration); // Time spent rendering
8 console.log('Base duration:', baseDuration); // Estimated time without memoization
9}
10
11function App() {
12 return (
13 <Profiler id="App" onRender={onRenderCallback}>
14 <MyComponent />
15 </Profiler>
16 );
17}
18
19// Custom performance monitoring
20function useRenderCount() {
21 const renderCount = useRef(0);
22 renderCount.current += 1;
23 console.log(`Render count: ${renderCount.current}`);
24}When NOT to Optimize:
Understanding these fundamentals helps you identify where optimization will provide real benefits versus where it might add unnecessary complexity.
useCallback memoizes functions to prevent unnecessary re-renders of child components that depend on function props.
Basic useCallback Usage:
1import { useCallback, useState, memo } from 'react';
2
3function ParentComponent() {
4 const [count, setCount] = useState(0);
5 const [name, setName] = useState('');
6
7 // Without useCallback: new function on every render
8 const handleClick = () => {
9 setCount(count + 1);
10 };
11
12 // With useCallback: same function reference when dependencies don't change
13 const handleClickOptimized = useCallback(() => {
14 setCount(prevCount => prevCount + 1);
15 }, []); // Empty dependencies since we use updater function
16
17 const handleNameChange = useCallback((newName) => {
18 setName(newName);
19 }, []); // Empty dependencies, stable function
20
21 return (
22 <div>
23 <input
24 value={name}
25 onChange={(e) => handleNameChange(e.target.value)}
26 />
27
28 {/* ChildComponent only re-renders when count changes */}
29 <ChildComponent
30 count={count}
31 onClick={handleClickOptimized}
32 />
33 </div>
34 );
35}
36
37// Memoized child component
38const ChildComponent = memo(({ count, onClick }) => {
39 console.log('ChildComponent rendered'); // Only logs when count changes
40
41 return (
42 <button onClick={onClick}>
43 Count: {count}
44 </button>
45 );
46});Advanced useCallback Patterns:
1. Event Handlers with Dependencies:
1function TodoList({ todos, filter }) {
2 const [selectedId, setSelectedId] = useState(null);
3
4 // Recreates when filter changes, stable otherwise
5 const handleTodoClick = useCallback((todoId) => {
6 if (filter === 'active') {
7 setSelectedId(todoId);
8 }
9 }, [filter]);
10
11 // Recreates when selectedId changes
12 const handleTodoToggle = useCallback((todoId) => {
13 // Toggle logic that depends on selectedId
14 if (selectedId === todoId) {
15 setSelectedId(null);
16 }
17 }, [selectedId]);
18
19 return (
20 <div>
21 {todos.map(todo => (
22 <TodoItem
23 key={todo.id}
24 todo={todo}
25 onClick={handleTodoClick}
26 onToggle={handleTodoToggle}
27 />
28 ))}
29 </div>
30 );
31}2. API Calls and Async Operations:
1function UserProfile({ userId }) {
2 const [user, setUser] = useState(null);
3 const [loading, setLoading] = useState(false);
4
5 // Memoized API call function
6 const fetchUser = useCallback(async (id) => {
7 setLoading(true);
8 try {
9 const response = await api.getUser(id);
10 setUser(response.data);
11 } catch (error) {
12 console.error('Failed to fetch user:', error);
13 } finally {
14 setLoading(false);
15 }
16 }, []); // No dependencies - stable function
17
18 // Memoized refresh function
19 const refreshUser = useCallback(() => {
20 if (userId) {
21 fetchUser(userId);
22 }
23 }, [userId, fetchUser]);
24
25 useEffect(() => {
26 refreshUser();
27 }, [refreshUser]);
28
29 return (
30 <div>
31 {loading ? 'Loading...' : user?.name}
32 <button onClick={refreshUser}>Refresh</button>
33 </div>
34 );
35}3. Custom Hook Integration:
1function useOptimizedHandlers(data, onUpdate) {
2 // Memoized handlers for different operations
3 const handleCreate = useCallback((newItem) => {
4 onUpdate([...data, { ...newItem, id: Date.now() }]);
5 }, [data, onUpdate]);
6
7 const handleUpdate = useCallback((id, updates) => {
8 onUpdate(data.map(item =>
9 item.id === id ? { ...item, ...updates } : item
10 ));
11 }, [data, onUpdate]);
12
13 const handleDelete = useCallback((id) => {
14 onUpdate(data.filter(item => item.id !== id));
15 }, [data, onUpdate]);
16
17 // Return memoized handlers object
18 return useMemo(() => ({
19 create: handleCreate,
20 update: handleUpdate,
21 delete: handleDelete
22 }), [handleCreate, handleUpdate, handleDelete]);
23}4. Dynamic Callback Generation:
1function DataGrid({ columns, data, onCellClick }) {
2 // Generate optimized click handlers for each cell
3 const getCellClickHandler = useCallback((rowIndex, columnId) => {
4 return (event) => {
5 onCellClick({
6 row: data[rowIndex],
7 column: columns.find(col => col.id === columnId),
8 value: data[rowIndex][columnId],
9 event
10 });
11 };
12 }, [data, columns, onCellClick]);
13
14 // Memoize handlers for each cell
15 const cellHandlers = useMemo(() => {
16 const handlers = new Map();
17 data.forEach((row, rowIndex) => {
18 columns.forEach(column => {
19 const key = `${rowIndex}-${column.id}`;
20 handlers.set(key, getCellClickHandler(rowIndex, column.id));
21 });
22 });
23 return handlers;
24 }, [data, columns, getCellClickHandler]);
25
26 return (
27 <table>
28 <tbody>
29 {data.map((row, rowIndex) => (
30 <tr key={row.id}>
31 {columns.map(column => (
32 <td
33 key={column.id}
34 onClick={cellHandlers.get(`${rowIndex}-${column.id}`)}
35 >
36 {row[column.id]}
37 </td>
38 ))}
39 </tr>
40 ))}
41 </tbody>
42 </table>
43 );
44}Common useCallback Mistakes:
1. Missing Dependencies:
1// Wrong: missing 'name' dependency
2const greetUser = useCallback(() => {
3 console.log(`Hello, ${name}!`); // Stale closure
4}, []);
5
6// Correct: include all dependencies
7const greetUser = useCallback(() => {
8 console.log(`Hello, ${name}!`);
9}, [name]);2. Overusing useCallback:
1// Unnecessary: child isn't memoized
2const handleClick = useCallback(() => {
3 doSomething();
4}, []);
5
6return <RegularComponent onClick={handleClick} />;
7
8// Unnecessary: no child components
9const handleInternalClick = useCallback(() => {
10 setInternalState(prev => !prev);
11}, []);useCallback is most effective when combined with memoized child components and when functions are passed as props to multiple components.
useMemo memoizes computed values and expensive calculations to prevent unnecessary recalculations on every render.
Basic useMemo Usage:
1import { useMemo, useState } from 'react';
2
3function ExpensiveComponent({ data, filter }) {
4 const [sort, setSort] = useState('name');
5
6 // Expensive computation memoized
7 const processedData = useMemo(() => {
8 console.log('Processing data...'); // Only logs when dependencies change
9
10 return data
11 .filter(item => item.category === filter)
12 .sort((a, b) => a[sort].localeCompare(b[sort]))
13 .map(item => ({
14 ...item,
15 displayName: `${item.name} (${item.category})`,
16 searchableText: `${item.name} ${item.description}`.toLowerCase()
17 }));
18 }, [data, filter, sort]); // Recalculates only when these change
19
20 // Derived state memoized
21 const statistics = useMemo(() => {
22 return {
23 total: processedData.length,
24 categories: [...new Set(processedData.map(item => item.category))],
25 averageRating: processedData.reduce((sum, item) => sum + item.rating, 0) / processedData.length
26 };
27 }, [processedData]);
28
29 return (
30 <div>
31 <div>Total: {statistics.total}</div>
32 <div>Average Rating: {statistics.averageRating.toFixed(2)}</div>
33
34 {processedData.map(item => (
35 <div key={item.id}>{item.displayName}</div>
36 ))}
37 </div>
38 );
39}Advanced useMemo Patterns:
1. Object and Array Stabilization:
1function ConfigurableChart({ rawData, settings }) {
2 // Stabilize configuration object
3 const chartConfig = useMemo(() => ({
4 type: settings.chartType,
5 options: {
6 responsive: true,
7 plugins: {
8 legend: { display: settings.showLegend },
9 tooltip: { enabled: settings.showTooltips }
10 },
11 scales: {
12 x: { display: settings.showXAxis },
13 y: { display: settings.showYAxis }
14 }
15 }
16 }), [settings.chartType, settings.showLegend, settings.showTooltips, settings.showXAxis, settings.showYAxis]);
17
18 // Memoize data transformation
19 const chartData = useMemo(() => {
20 return {
21 labels: rawData.map(item => item.label),
22 datasets: [{
23 label: 'Values',
24 data: rawData.map(item => item.value),
25 backgroundColor: settings.colorScheme.map((color, index) =>
26 color || `hsl(${index * 30}, 70%, 50%)`
27 )
28 }]
29 };
30 }, [rawData, settings.colorScheme]);
31
32 return <Chart type={chartConfig.type} data={chartData} options={chartConfig.options} />;
33}2. Complex Search and Filtering:
1function AdvancedSearch({ items, searchQuery, filters }) {
2 // Memoize search index for performance
3 const searchIndex = useMemo(() => {
4 const index = new Map();
5
6 items.forEach(item => {
7 const searchableFields = [
8 item.title,
9 item.description,
10 item.tags?.join(' '),
11 item.author?.name
12 ].filter(Boolean);
13
14 const searchableText = searchableFields.join(' ').toLowerCase();
15 index.set(item.id, searchableText);
16 });
17
18 return index;
19 }, [items]);
20
21 // Memoize filtered and searched results
22 const filteredResults = useMemo(() => {
23 let results = items;
24
25 // Apply filters
26 if (filters.category) {
27 results = results.filter(item => item.category === filters.category);
28 }
29 if (filters.dateRange) {
30 results = results.filter(item =>
31 new Date(item.createdAt) >= filters.dateRange.start &&
32 new Date(item.createdAt) <= filters.dateRange.end
33 );
34 }
35 if (filters.rating) {
36 results = results.filter(item => item.rating >= filters.rating);
37 }
38
39 // Apply search
40 if (searchQuery) {
41 const query = searchQuery.toLowerCase();
42 results = results.filter(item => {
43 const searchableText = searchIndex.get(item.id);
44 return searchableText?.includes(query);
45 });
46 }
47
48 return results;
49 }, [items, searchQuery, filters, searchIndex]);
50
51 // Memoize result statistics
52 const resultStats = useMemo(() => ({
53 total: filteredResults.length,
54 categories: countBy(filteredResults, 'category'),
55 averageRating: filteredResults.reduce((sum, item) => sum + item.rating, 0) / filteredResults.length,
56 dateRange: {
57 earliest: Math.min(...filteredResults.map(item => new Date(item.createdAt).getTime())),
58 latest: Math.max(...filteredResults.map(item => new Date(item.createdAt).getTime()))
59 }
60 }), [filteredResults]);
61
62 return (
63 <div>
64 <SearchStats stats={resultStats} />
65 <SearchResults results={filteredResults} />
66 </div>
67 );
68}3. Memoized Context Values:
1function ThemeProvider({ children }) {
2 const [theme, setTheme] = useState('light');
3 const [customColors, setCustomColors] = useState({});
4
5 // Memoize context value to prevent unnecessary re-renders
6 const themeContextValue = useMemo(() => {
7 const baseTheme = themes[theme];
8 const mergedTheme = {
9 ...baseTheme,
10 colors: {
11 ...baseTheme.colors,
12 ...customColors
13 }
14 };
15
16 return {
17 theme: mergedTheme,
18 themeName: theme,
19 setTheme,
20 updateCustomColor: (colorKey, colorValue) => {
21 setCustomColors(prev => ({ ...prev, [colorKey]: colorValue }));
22 },
23 resetCustomColors: () => setCustomColors({})
24 };
25 }, [theme, customColors]);
26
27 return (
28 <ThemeContext.Provider value={themeContextValue}>
29 {children}
30 </ThemeContext.Provider>
31 );
32}4. Dynamic Form Validation:
1function DynamicForm({ schema, values, onChange }) {
2 // Memoize validation rules
3 const validationRules = useMemo(() => {
4 const rules = new Map();
5
6 schema.forEach(field => {
7 const fieldRules = [];
8
9 if (field.required) {
10 fieldRules.push(value => value ? null : 'This field is required');
11 }
12
13 if (field.minLength) {
14 fieldRules.push(value =>
15 value && value.length >= field.minLength ? null : `Minimum length is ${field.minLength}`
16 );
17 }
18
19 if (field.pattern) {
20 fieldRules.push(value =>
21 !value || field.pattern.test(value) ? null : field.patternMessage || 'Invalid format'
22 );
23 }
24
25 if (field.customValidator) {
26 fieldRules.push(field.customValidator);
27 }
28
29 rules.set(field.name, fieldRules);
30 });
31
32 return rules;
33 }, [schema]);
34
35 // Memoize validation results
36 const validationResults = useMemo(() => {
37 const results = {};
38
39 schema.forEach(field => {
40 const value = values[field.name];
41 const rules = validationRules.get(field.name) || [];
42 const errors = [];
43
44 rules.forEach(rule => {
45 const error = rule(value, values); // Pass all values for cross-field validation
46 if (error) errors.push(error);
47 });
48
49 results[field.name] = {
50 isValid: errors.length === 0,
51 errors
52 };
53 });
54
55 return results;
56 }, [schema, values, validationRules]);
57
58 // Memoize form state
59 const formState = useMemo(() => ({
60 isValid: Object.values(validationResults).every(result => result.isValid),
61 errors: Object.entries(validationResults).reduce((acc, [field, result]) => {
62 if (result.errors.length > 0) {
63 acc[field] = result.errors;
64 }
65 return acc;
66 }, {}),
67 touchedFields: Object.keys(values)
68 }), [validationResults, values]);
69
70 return (
71 <FormContext.Provider value={{ validationResults, formState }}>
72 {schema.map(field => (
73 <FormField
74 key={field.name}
75 field={field}
76 value={values[field.name]}
77 onChange={(value) => onChange(field.name, value)}
78 />
79 ))}
80 </FormContext.Provider>
81 );
82}Performance Monitoring for useMemo:
1function useExpensiveComputation(data, dependencies) {
2 return useMemo(() => {
3 const startTime = performance.now();
4 const result = heavyComputation(data);
5 const endTime = performance.now();
6
7 console.log(`Computation took ${endTime - startTime} milliseconds`);
8 return result;
9 }, dependencies);
10}useMemo is most effective for expensive computations, object/array stabilization, and preventing cascading re-renders in complex component trees.
A comprehensive demonstration of useCallback and useMemo optimization techniques in a complex data dashboard with filtering, sorting, real-time updates, and performance monitoring.
1import React, { useState, useCallback, useMemo, memo, useRef, useEffect } from 'react';
2
3// =====================================================================
4// Performance Monitoring Utilities
5// =====================================================================
6
7function useRenderCount(componentName) {
8 const renderCount = useRef(0);
9 const startTime = useRef(performance.now());
10
11 renderCount.current += 1;
12
13 useEffect(() => {
14 const endTime = performance.now();
15 console.log(`[${componentName}] Render #${renderCount.current} took ${(endTime - startTime.current).toFixed(2)}ms`);
16 startTime.current = performance.now();
17 });
18
19 return renderCount.current;
20}
21
22function usePerfProfiler(name) {
23 const startTime = useRef(performance.now());
24
25 useEffect(() => {
26 const endTime = performance.now();
27 if (endTime - startTime.current > 16) { // Highlight slow renders (>16ms for 60fps)
28 console.warn(`🐌 Slow render detected in ${name}: ${(endTime - startTime.current).toFixed(2)}ms`);
29 }
30 startTime.current = performance.now();
31 });
32}
33
34// =====================================================================
35// Mock Data Generation
36// =====================================================================
37
38function generateMockData(count = 1000) {
39 const categories = ['Technology', 'Healthcare', 'Finance', 'Education', 'Retail'];
40 const statuses = ['active', 'inactive', 'pending', 'completed'];
41
42 return Array.from({ length: count }, (_, i) => ({
43 id: i + 1,
44 name: `Item ${i + 1}`,
45 category: categories[Math.floor(Math.random() * categories.length)],
46 status: statuses[Math.floor(Math.random() * statuses.length)],
47 value: Math.floor(Math.random() * 10000),
48 rating: Math.random() * 5,
49 createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
50 tags: Array.from({ length: Math.floor(Math.random() * 5) + 1 }, () =>
51 `tag${Math.floor(Math.random() * 50)}`
52 ),
53 metadata: {
54 priority: Math.floor(Math.random() * 5) + 1,
55 complexity: Math.random(),
56 estimatedHours: Math.floor(Math.random() * 100) + 1
57 }
58 }));
59}
60
61// =====================================================================
62// Optimized Components
63// =====================================================================
64
65// Memoized list item component
66const DataListItem = memo(function DataListItem({ item, onUpdate, onDelete, isSelected, onSelect }) {
67 const renderCount = useRenderCount(`DataListItem-${item.id}`);
68
69 // Memoized click handlers
70 const handleSelect = useCallback(() => {
71 onSelect(item.id);
72 }, [item.id, onSelect]);
73
74 const handleStatusUpdate = useCallback(() => {
75 const newStatus = item.status === 'active' ? 'inactive' : 'active';
76 onUpdate(item.id, { status: newStatus });
77 }, [item.id, item.status, onUpdate]);
78
79 const handleDelete = useCallback(() => {
80 onDelete(item.id);
81 }, [item.id, onDelete]);
82
83 // Memoized styles based on item properties
84 const itemStyles = useMemo(() => ({
85 backgroundColor: isSelected ? '#e3f2fd' : 'transparent',
86 borderLeft: `4px solid ${
87 item.status === 'active' ? '#4caf50' :
88 item.status === 'inactive' ? '#f44336' :
89 item.status === 'pending' ? '#ff9800' : '#2196f3'
90 }`,
91 opacity: item.status === 'inactive' ? 0.6 : 1
92 }), [isSelected, item.status]);
93
94 return (
95 <div
96 style={itemStyles}
97 className="p-4 border-b cursor-pointer hover:bg-gray-50 transition-colors"
98 onClick={handleSelect}
99 >
100 <div className="flex items-center justify-between">
101 <div className="flex-1">
102 <div className="flex items-center gap-3 mb-2">
103 <h3 className="font-semibold">{item.name}</h3>
104 <span className="text-xs text-gray-500">Renders: {renderCount}</span>
105 <span className={`px-2 py-1 text-xs rounded ${
106 item.status === 'active' ? 'bg-green-100 text-green-800' :
107 item.status === 'inactive' ? 'bg-red-100 text-red-800' :
108 item.status === 'pending' ? 'bg-orange-100 text-orange-800' :
109 'bg-blue-100 text-blue-800'
110 }`}>
111 {item.status}
112 </span>
113 </div>
114
115 <div className="text-sm text-gray-600 space-y-1">
116 <div>Category: {item.category} | Value: ${item.value.toLocaleString()}</div>
117 <div>Rating: {item.rating.toFixed(1)}⭐ | Priority: {item.metadata.priority}</div>
118 <div>Tags: {item.tags.slice(0, 3).join(', ')}{item.tags.length > 3 ? '...' : ''}</div>
119 </div>
120 </div>
121
122 <div className="flex gap-2">
123 <button
124 onClick={(e) => {
125 e.stopPropagation();
126 handleStatusUpdate();
127 }}
128 className="px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600"
129 >
130 Toggle Status
131 </button>
132 <button
133 onClick={(e) => {
134 e.stopPropagation();
135 handleDelete();
136 }}
137 className="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
138 >
139 Delete
140 </button>
141 </div>
142 </div>
143 </div>
144 );
145});
146
147// Memoized statistics component
148const StatisticsPanel = memo(function StatisticsPanel({ data, selectedItems }) {
149 usePerfProfiler('StatisticsPanel');
150
151 // Expensive statistics calculations memoized
152 const statistics = useMemo(() => {
153 console.log('📊 Calculating statistics...');
154
155 const categoryStats = data.reduce((acc, item) => {
156 acc[item.category] = (acc[item.category] || 0) + 1;
157 return acc;
158 }, {});
159
160 const statusStats = data.reduce((acc, item) => {
161 acc[item.status] = (acc[item.status] || 0) + 1;
162 return acc;
163 }, {});
164
165 const valueStats = {
166 total: data.reduce((sum, item) => sum + item.value, 0),
167 average: data.length > 0 ? data.reduce((sum, item) => sum + item.value, 0) / data.length : 0,
168 max: Math.max(...data.map(item => item.value)),
169 min: Math.min(...data.map(item => item.value))
170 };
171
172 const ratingStats = {
173 average: data.length > 0 ? data.reduce((sum, item) => sum + item.rating, 0) / data.length : 0,
174 distribution: [1, 2, 3, 4, 5].map(rating =>
175 data.filter(item => Math.floor(item.rating) + 1 === rating).length
176 )
177 };
178
179 return {
180 total: data.length,
181 categories: categoryStats,
182 statuses: statusStats,
183 values: valueStats,
184 ratings: ratingStats,
185 selected: selectedItems.length
186 };
187 }, [data, selectedItems]);
188
189 return (
190 <div className="bg-white p-6 rounded-lg shadow-lg">
191 <h2 className="text-xl font-bold mb-4">📈 Statistics</h2>
192
193 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
194 <div className="text-center">
195 <div className="text-2xl font-bold text-blue-600">{statistics.total}</div>
196 <div className="text-sm text-gray-600">Total Items</div>
197 </div>
198 <div className="text-center">
199 <div className="text-2xl font-bold text-green-600">{statistics.selected}</div>
200 <div className="text-sm text-gray-600">Selected</div>
201 </div>
202 <div className="text-center">
203 <div className="text-2xl font-bold text-purple-600">
204 ${statistics.values.total.toLocaleString()}
205 </div>
206 <div className="text-sm text-gray-600">Total Value</div>
207 </div>
208 <div className="text-center">
209 <div className="text-2xl font-bold text-orange-600">
210 {statistics.ratings.average.toFixed(1)}⭐
211 </div>
212 <div className="text-sm text-gray-600">Avg Rating</div>
213 </div>
214 </div>
215
216 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
217 <div>
218 <h3 className="font-semibold mb-2">Categories</h3>
219 {Object.entries(statistics.categories).map(([category, count]) => (
220 <div key={category} className="flex justify-between text-sm mb-1">
221 <span>{category}</span>
222 <span>{count}</span>
223 </div>
224 ))}
225 </div>
226
227 <div>
228 <h3 className="font-semibold mb-2">Status Distribution</h3>
229 {Object.entries(statistics.statuses).map(([status, count]) => (
230 <div key={status} className="flex justify-between text-sm mb-1">
231 <span className="capitalize">{status}</span>
232 <span>{count}</span>
233 </div>
234 ))}
235 </div>
236 </div>
237 </div>
238 );
239});
240
241// Memoized filter panel
242const FilterPanel = memo(function FilterPanel({
243 filters,
244 onFiltersChange,
245 availableCategories,
246 onResetFilters
247}) {
248 usePerfProfiler('FilterPanel');
249
250 // Memoized filter change handlers
251 const handleCategoryChange = useCallback((category) => {
252 onFiltersChange({ ...filters, category });
253 }, [filters, onFiltersChange]);
254
255 const handleStatusChange = useCallback((status) => {
256 onFiltersChange({ ...filters, status });
257 }, [filters, onFiltersChange]);
258
259 const handleSearchChange = useCallback((search) => {
260 onFiltersChange({ ...filters, search });
261 }, [filters, onFiltersChange]);
262
263 const handleMinValueChange = useCallback((minValue) => {
264 onFiltersChange({ ...filters, minValue: parseInt(minValue) || 0 });
265 }, [filters, onFiltersChange]);
266
267 return (
268 <div className="bg-white p-6 rounded-lg shadow-lg mb-6">
269 <div className="flex items-center justify-between mb-4">
270 <h2 className="text-xl font-bold">🔍 Filters</h2>
271 <button
272 onClick={onResetFilters}
273 className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600"
274 >
275 Reset Filters
276 </button>
277 </div>
278
279 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
280 <div>
281 <label className="block text-sm font-medium mb-1">Search</label>
282 <input
283 type="text"
284 value={filters.search}
285 onChange={(e) => handleSearchChange(e.target.value)}
286 placeholder="Search items..."
287 className="w-full p-2 border rounded"
288 />
289 </div>
290
291 <div>
292 <label className="block text-sm font-medium mb-1">Category</label>
293 <select
294 value={filters.category}
295 onChange={(e) => handleCategoryChange(e.target.value)}
296 className="w-full p-2 border rounded"
297 >
298 <option value="">All Categories</option>
299 {availableCategories.map(category => (
300 <option key={category} value={category}>{category}</option>
301 ))}
302 </select>
303 </div>
304
305 <div>
306 <label className="block text-sm font-medium mb-1">Status</label>
307 <select
308 value={filters.status}
309 onChange={(e) => handleStatusChange(e.target.value)}
310 className="w-full p-2 border rounded"
311 >
312 <option value="">All Statuses</option>
313 <option value="active">Active</option>
314 <option value="inactive">Inactive</option>
315 <option value="pending">Pending</option>
316 <option value="completed">Completed</option>
317 </select>
318 </div>
319
320 <div>
321 <label className="block text-sm font-medium mb-1">Min Value</label>
322 <input
323 type="number"
324 value={filters.minValue}
325 onChange={(e) => handleMinValueChange(e.target.value)}
326 placeholder="0"
327 className="w-full p-2 border rounded"
328 />
329 </div>
330 </div>
331 </div>
332 );
333});
334
335// =====================================================================
336// Main Dashboard Component
337// =====================================================================
338
339function DataDashboard() {
340 // State management
341 const [rawData] = useState(() => generateMockData(1000));
342 const [filters, setFilters] = useState({
343 search: '',
344 category: '',
345 status: '',
346 minValue: 0
347 });
348 const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
349 const [selectedItems, setSelectedItems] = useState(new Set());
350
351 usePerfProfiler('DataDashboard');
352
353 // Memoized available categories
354 const availableCategories = useMemo(() => {
355 return [...new Set(rawData.map(item => item.category))].sort();
356 }, [rawData]);
357
358 // Memoized filtered data
359 const filteredData = useMemo(() => {
360 console.log('🔄 Filtering data...');
361
362 return rawData.filter(item => {
363 if (filters.search && !item.name.toLowerCase().includes(filters.search.toLowerCase())) {
364 return false;
365 }
366 if (filters.category && item.category !== filters.category) {
367 return false;
368 }
369 if (filters.status && item.status !== filters.status) {
370 return false;
371 }
372 if (filters.minValue && item.value < filters.minValue) {
373 return false;
374 }
375 return true;
376 });
377 }, [rawData, filters]);
378
379 // Memoized sorted data
380 const sortedData = useMemo(() => {
381 console.log('🔄 Sorting data...');
382
383 const sorted = [...filteredData].sort((a, b) => {
384 const aValue = a[sortConfig.key];
385 const bValue = b[sortConfig.key];
386
387 if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
388 if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
389 return 0;
390 });
391
392 return sorted;
393 }, [filteredData, sortConfig]);
394
395 // Memoized event handlers
396 const handleFiltersChange = useCallback((newFilters) => {
397 setFilters(newFilters);
398 }, []);
399
400 const handleResetFilters = useCallback(() => {
401 setFilters({
402 search: '',
403 category: '',
404 status: '',
405 minValue: 0
406 });
407 }, []);
408
409 const handleSort = useCallback((key) => {
410 setSortConfig(prev => ({
411 key,
412 direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
413 }));
414 }, []);
415
416 const handleItemUpdate = useCallback((itemId, updates) => {
417 // In a real app, this would update the backend
418 console.log(`Updating item ${itemId}:, updates`);
419 }, []);
420
421 const handleItemDelete = useCallback((itemId) => {
422 // In a real app, this would delete from backend
423 console.log(`Deleting item ${itemId}`);
424 }, []);
425
426 const handleItemSelect = useCallback((itemId) => {
427 setSelectedItems(prev => {
428 const newSelected = new Set(prev);
429 if (newSelected.has(itemId)) {
430 newSelected.delete(itemId);
431 } else {
432 newSelected.add(itemId);
433 }
434 return newSelected;
435 });
436 }, []);
437
438 const handleSelectAll = useCallback(() => {
439 setSelectedItems(new Set(sortedData.map(item => item.id)));
440 }, [sortedData]);
441
442 const handleDeselectAll = useCallback(() => {
443 setSelectedItems(new Set());
444 }, []);
445
446 // Memoized selected items array for statistics
447 const selectedItemsArray = useMemo(() => {
448 return Array.from(selectedItems);
449 }, [selectedItems]);
450
451 return (
452 <div className="min-h-screen bg-gray-100 p-6">
453 <div className="max-w-7xl mx-auto">
454 <div className="mb-8">
455 <h1 className="text-3xl font-bold text-gray-800 mb-2">
456 📊 Performance Optimized Data Dashboard
457 </h1>
458 <p className="text-gray-600">
459 Demonstration of useCallback and useMemo optimization with {rawData.length} items
460 </p>
461 </div>
462
463 <FilterPanel
464 filters={filters}
465 onFiltersChange={handleFiltersChange}
466 availableCategories={availableCategories}
467 onResetFilters={handleResetFilters}
468 />
469
470 <StatisticsPanel
471 data={sortedData}
472 selectedItems={selectedItemsArray}
473 />
474
475 <div className="bg-white rounded-lg shadow-lg mt-6">
476 <div className="p-6 border-b">
477 <div className="flex items-center justify-between">
478 <div>
479 <h2 className="text-xl font-bold">📋 Data List</h2>
480 <p className="text-sm text-gray-600">
481 Showing {sortedData.length} of {rawData.length} items
482 </p>
483 </div>
484
485 <div className="flex gap-2">
486 <button
487 onClick={handleSelectAll}
488 className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
489 >
490 Select All
491 </button>
492 <button
493 onClick={handleDeselectAll}
494 className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600"
495 >
496 Deselect All
497 </button>
498 </div>
499 </div>
500
501 <div className="flex gap-4 mt-4">
502 <button
503 onClick={() => handleSort('name')}
504 className={`px-3 py-1 text-sm rounded ${
505 sortConfig.key === 'name' ? 'bg-blue-500 text-white' : 'bg-gray-200'
506 }`}
507 >
508 Sort by Name {sortConfig.key === 'name' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
509 </button>
510 <button
511 onClick={() => handleSort('value')}
512 className={`px-3 py-1 text-sm rounded ${
513 sortConfig.key === 'value' ? 'bg-blue-500 text-white' : 'bg-gray-200'
514 }`}
515 >
516 Sort by Value {sortConfig.key === 'value' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
517 </button>
518 <button
519 onClick={() => handleSort('rating')}
520 className={`px-3 py-1 text-sm rounded ${
521 sortConfig.key === 'rating' ? 'bg-blue-500 text-white' : 'bg-gray-200'
522 }`}
523 >
524 Sort by Rating {sortConfig.key === 'rating' && (sortConfig.direction === 'asc' ? '↑' : '↓')}
525 </button>
526 </div>
527 </div>
528
529 <div className="max-h-96 overflow-y-auto">
530 {sortedData.map(item => (
531 <DataListItem
532 key={item.id}
533 item={item}
534 onUpdate={handleItemUpdate}
535 onDelete={handleItemDelete}
536 isSelected={selectedItems.has(item.id)}
537 onSelect={handleItemSelect}
538 />
539 ))}
540 </div>
541 </div>
542
543 <div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
544 <h3 className="font-semibold text-yellow-800 mb-2">🎯 Performance Tips</h3>
545 <ul className="text-sm text-yellow-700 space-y-1">
546 <li>• Watch the console for render counts and timing information</li>
547 <li>• Notice how memoized components only re-render when their props change</li>
548 <li>• Expensive calculations (filtering, sorting, statistics) are cached</li>
549 <li>• Event handlers are stable between renders using useCallback</li>
550 <li>• Open React DevTools Profiler to see detailed performance metrics</li>
551 </ul>
552 </div>
553 </div>
554 </div>
555 );
556}
557
558export default DataDashboard;This comprehensive performance optimization demonstration showcases advanced useCallback and useMemo patterns in a real-world scenario:
🚀 Performance Optimization Highlights:
1. Strategic Component Memoization:
2. Advanced useMemo Patterns:
3. Sophisticated useCallback Usage:
4. Real-World Optimization Scenarios:
5. Performance Monitoring Features:
6. Production-Ready Patterns:
7. Advanced Optimization Techniques:
This demonstration shows how proper use of useCallback and useMemo can maintain smooth performance even with complex data operations and frequent UI updates.
Excellent work mastering performance optimization with useCallback and useMemo! You now understand how to build highly optimized React applications that maintain smooth performance under complex scenarios. In our next lesson, we'll explore useRef and DOM Manipulation, where you'll learn advanced ref patterns, imperative DOM operations, and how to integrate with third-party libraries while maintaining React best practices.
Your progress is saved in this browser, on this device only.