React's performance optimization hooks useMemo
and useCallback
are essential tools for building efficient applications, yet they're often misunderstood and overused. This comprehensive guide will teach you when, why, and how to use these hooks effectively, with real-world examples and best practices.
Before diving into performance hooks, it's crucial to understand how React works. Every time your component's state or props change, React triggers a re-render process:
๐ The key insight is that every variable, function, and object is recreated on each render, regardless of whether their values have actually changed.
jsx1const UserProfile = ({ userId }) => { 2 const [user, setUser] = useState(null); 3 const [preferences, setPreferences] = useState({}); 4 5 // These are ALL recreated on every render ๐ 6 const userSettings = { theme: 'dark', language: 'en' }; 7 const handleSave = () => saveUserData(user); 8 const fullName = user ? `${user.firstName} ${user.lastName}` : ''; 9 10 return ( 11 <div> 12 <h1>{fullName}</h1> 13 <Settings config={userSettings} /> 14 <SaveButton onClick={handleSave} /> 15 </div> 16 ); 17};
Consider this real-world example of a data analytics dashboard:
jsx1const SalesAnalytics = () => { 2 const [salesData, setSalesData] = useState([]); 3 const [dateRange, setDateRange] = useState({ start: null, end: null }); 4 const [refreshCounter, setRefreshCounter] = useState(0); 5 6 // This expensive calculation runs on EVERY render ๐ธ 7 const analytics = calculateSalesMetrics(salesData, dateRange); 8 9 // Auto-refresh every 30 seconds โฐ 10 useEffect(() => { 11 const interval = setInterval(() => { 12 setRefreshCounter((prev) => prev + 1); 13 }, 30000); 14 return () => clearInterval(interval); 15 }, []); 16 17 return ( 18 <div> 19 <p>๐ Last updated: {new Date().toLocaleTimeString()}</p> 20 <MetricsDisplay data={analytics} /> 21 <DateRangeSelector onChange={setDateRange} /> 22 </div> 23 ); 24}; 25 26const calculateSalesMetrics = (data, dateRange) => { 27 console.log('๐ Calculating expensive metrics...'); 28 29 // Simulate expensive operations ๐ 30 const filtered = data.filter((sale) => { 31 const saleDate = new Date(sale.date); 32 return ( 33 (!dateRange.start || saleDate >= dateRange.start) && 34 (!dateRange.end || saleDate <= dateRange.end) 35 ); 36 }); 37 38 return { 39 totalRevenue: filtered.reduce((sum, sale) => sum + sale.amount, 0), 40 averageOrderValue: 41 filtered.reduce((sum, sale) => sum + sale.amount, 0) / filtered.length, 42 topProducts: getTopProducts(filtered), 43 monthlyTrends: calculateTrends(filtered), 44 }; 45};
โ ๏ธ The Problem: The expensive calculateSalesMetrics
function runs every 30 seconds when refreshCounter
updates, even though salesData
and dateRange
haven't changed.
jsx1const SalesAnalytics = () => { 2 const [salesData, setSalesData] = useState([]); 3 const [dateRange, setDateRange] = useState({ start: null, end: null }); 4 const [refreshCounter, setRefreshCounter] = useState(0); 5 6 // Only recalculate when salesData or dateRange change ๐ฏ 7 const analytics = useMemo(() => { 8 console.log('๐ Calculating metrics...'); // You'll see this much less now ๐ 9 return calculateSalesMetrics(salesData, dateRange); 10 }, [salesData, dateRange]); 11 12 // Auto-refresh logic remains the same โฐ 13 useEffect(() => { 14 const interval = setInterval(() => { 15 setRefreshCounter((prev) => prev + 1); 16 }, 30000); 17 return () => clearInterval(interval); 18 }, []); 19 20 return ( 21 <div> 22 <p>๐ Last updated: {new Date().toLocaleTimeString()}</p> 23 <MetricsDisplay data={analytics} /> 24 <DateRangeSelector onChange={setDateRange} /> 25 </div> 26 ); 27};
๐ Now the expensive calculation only runs when the actual dependencies change, not on every render.
Here's a common scenario with an e-commerce shopping cart:
jsx1const ShoppingCart = () => { 2 const [items, setItems] = useState([]); 3 const [customerInfo, setCustomerInfo] = useState({}); 4 const [promoCode, setPromoCode] = useState(''); 5 6 // This object is recreated on every render ๐ 7 const cartSummary = { 8 itemCount: items.length, 9 subtotal: items.reduce((sum, item) => sum + item.price * item.quantity, 0), 10 tax: 0, 11 total: 0, 12 }; 13 14 // Calculate tax and total ๐งฎ 15 cartSummary.tax = cartSummary.subtotal * 0.08; 16 cartSummary.total = cartSummary.subtotal + cartSummary.tax; 17 18 const handleRemoveItem = (itemId) => { 19 setItems((prev) => prev.filter((item) => item.id !== itemId)); 20 }; 21 22 return ( 23 <div> 24 <ItemList items={items} onRemoveItem={handleRemoveItem} /> 25 <CartSummary summary={cartSummary} /> 26 <PromoCodeInput value={promoCode} onChange={setPromoCode} /> 27 </div> 28 ); 29}; 30 31// This component should only re-render when summary changes ๐ฏ 32const CartSummary = React.memo(({ summary }) => { 33 console.log('๐ CartSummary rendered'); 34 return ( 35 <div className="cart-summary"> 36 <h3>๐ Order Summary</h3> 37 <p>๐ฆ Items: {summary.itemCount}</p> 38 <p>๐ฐ Subtotal: ${summary.subtotal.toFixed(2)}</p> 39 <p>๐ท๏ธ Tax: ${summary.tax.toFixed(2)}</p> 40 <p>๐ณ Total: ${summary.total.toFixed(2)}</p> 41 </div> 42 ); 43});
โ ๏ธ The Problem: Even though CartSummary
is wrapped in React.memo
, it re-renders whenever promoCode
changes because carrtSummary
is a new object reference every time.
jsx1const ShoppingCart = () => { 2 const [items, setItems] = useState([]); 3 const [customerInfo, setCustomerInfo] = useState({}); 4 const [promoCode, setPromoCode] = useState(''); 5 6 // Memoize the cart summary calculation ๐ฏ 7 const cartSummary = useMemo(() => { 8 const subtotal = items.reduce( 9 (sum, item) => sum + item.price * item.quantity, 10 0 11 ); 12 const tax = subtotal * 0.08; 13 14 return { 15 itemCount: items.length, 16 subtotal, 17 tax, 18 total: subtotal + tax, 19 }; 20 }, [items]); // Only recalculate when items change โ 21 22 const handleRemoveItem = (itemId) => { 23 setItems((prev) => prev.filter((item) => item.id !== itemId)); 24 }; 25 26 return ( 27 <div> 28 <ItemList items={items} onRemoveItem={handleRemoveItem} /> 29 <CartSummary summary={cartSummary} /> 30 <PromoCodeInput value={promoCode} onChange={setPromoCode} /> 31 </div> 32 ); 33};
๐ง Now CartSummary
only re-renders when the cart items actually change, not when the promo code is typed.
Functions suffer from the same reference problem. Here's a task management example:
jsx1const TaskManager = () => { 2 const [tasks, setTasks] = useState([]); 3 const [filter, setFilter] = useState('all'); 4 const [searchTerm, setSearchTerm] = useState(''); 5 6 // These functions are recreated on every render ๐ 7 const addTask = (text) => { 8 const newTask = { 9 id: Date.now(), 10 text, 11 completed: false, 12 createdAt: new Date(), 13 }; 14 setTasks((prev) => [...prev, newTask]); 15 }; 16 17 const toggleTask = (id) => { 18 setTasks((prev) => 19 prev.map((task) => 20 task.id === id ? { ...task, completed: !task.completed } : task 21 ) 22 ); 23 }; 24 25 const deleteTask = (id) => { 26 setTasks((prev) => prev.filter((task) => task.id !== id)); 27 }; 28 29 // Filter tasks based on current filter and search term ๐ 30 const filteredTasks = useMemo(() => { 31 return tasks.filter((task) => { 32 const matchesFilter = 33 filter === 'all' || 34 (filter === 'completed' && task.completed) || 35 (filter === 'pending' && !task.completed); 36 37 const matchesSearch = task.text 38 .toLowerCase() 39 .includes(searchTerm.toLowerCase()); 40 41 return matchesFilter && matchesSearch; 42 }); 43 }, [tasks, filter, searchTerm]); 44 45 return ( 46 <div> 47 <TaskInput onAddTask={addTask} /> 48 <TaskFilters filter={filter} onFilterChange={setFilter} /> 49 <SearchInput value={searchTerm} onChange={setSearchTerm} /> 50 <TaskList 51 tasks={filteredTasks} 52 onToggle={toggleTask} 53 onDelete={deleteTask} 54 /> 55 </div> 56 ); 57}; 58 59// These components are memoized but still re-render unnecessarily ๐ 60const TaskInput = React.memo(({ onAddTask }) => { 61 const [input, setInput] = useState(''); 62 63 const handleSubmit = (e) => { 64 e.preventDefault(); 65 if (input.trim()) { 66 onAddTask(input.trim()); 67 setInput(''); 68 } 69 }; 70 71 return ( 72 <form onSubmit={handleSubmit}> 73 <input 74 value={input} 75 onChange={(e) => setInput(e.target.value)} 76 placeholder="โ Add a new task..." 77 /> 78 <button type="submit">Add</button> 79 </form> 80 ); 81}); 82 83const TaskList = React.memo(({ tasks, onToggle, onDelete }) => { 84 console.log('๐ TaskList rendered'); 85 return ( 86 <div> 87 {tasks.map((task) => ( 88 <TaskItem 89 key={task.id} 90 task={task} 91 onToggle={onToggle} 92 onDelete={onDelete} 93 /> 94 ))} 95 </div> 96 ); 97});
โ ๏ธ The Problem: TaskInput
and TaskList
re-render whenever any state changes because their function props are new references each time.
jsx1const TaskManager = () => { 2 const [tasks, setTasks] = useState([]); 3 const [filter, setFilter] = useState('all'); 4 const [searchTerm, setSearchTerm] = useState(''); 5 6 // Memoize functions with useCallback ๐ฏ 7 const addTask = useCallback((text) => { 8 const newTask = { 9 id: Date.now(), 10 text, 11 completed: false, 12 createdAt: new Date(), 13 }; 14 setTasks((prev) => [...prev, newTask]); 15 }, []); // Empty dependency array because we use functional update โ 16 17 const toggleTask = useCallback((id) => { 18 setTasks((prev) => 19 prev.map((task) => 20 task.id === id ? { ...task, completed: !task.completed } : task 21 ) 22 ); 23 }, []); // Empty dependency array because we use functional update โ 24 25 const deleteTask = useCallback((id) => { 26 setTasks((prev) => prev.filter((task) => task.id !== id)); 27 }, []); // Empty dependency array because we use functional update โ 28 29 // Filter tasks (using useMemo as before) ๐ 30 const filteredTasks = useMemo(() => { 31 return tasks.filter((task) => { 32 const matchesFilter = 33 filter === 'all' || 34 (filter === 'completed' && task.completed) || 35 (filter === 'pending' && !task.completed); 36 37 const matchesSearch = task.text 38 .toLowerCase() 39 .includes(searchTerm.toLowerCase()); 40 41 return matchesFilter && matchesSearch; 42 }); 43 }, [tasks, filter, searchTerm]); 44 45 return ( 46 <div> 47 <TaskInput onAddTask={addTask} /> 48 <TaskFilters filter={filter} onFilterChange={setFilter} /> 49 <SearchInput value={searchTerm} onChange={setSearchTerm} /> 50 <TaskList 51 tasks={filteredTasks} 52 onToggle={toggleTask} 53 onDelete={deleteTask} 54 /> 55 </div> 56 ); 57};
๐ก Key Insight: By using functional updates (prev => ...
), we avoid including current state in the dependency array, making our callbacks more stable.
Here's a comprehensive example showing both hooks working together in a data visualization dashboard:
jsx1const DataVisualization = () => { 2 const [rawData, setRawData] = useState([]); 3 const [chartType, setChartType] = useState('line'); 4 const [dateRange, setDateRange] = useState({ start: null, end: null }); 5 const [groupBy, setGroupBy] = useState('day'); 6 const [selectedMetrics, setSelectedMetrics] = useState(['revenue', 'users']); 7 8 // Complex data processing with useMemo ๐ 9 const processedData = useMemo(() => { 10 console.log('๐ Processing chart data...'); 11 12 // Filter by date range ๐ 13 const filteredData = rawData.filter((item) => { 14 const date = new Date(item.timestamp); 15 return ( 16 (!dateRange.start || date >= dateRange.start) && 17 (!dateRange.end || date <= dateRange.end) 18 ); 19 }); 20 21 // Group data by specified period ๐ 22 const grouped = filteredData.reduce((acc, item) => { 23 const key = getGroupKey(item.timestamp, groupBy); 24 if (!acc[key]) acc[key] = []; 25 acc[key].push(item); 26 return acc; 27 }, {}); 28 29 // Calculate metrics for each group ๐ 30 return Object.entries(grouped) 31 .map(([period, items]) => { 32 const dataPoint = { period }; 33 34 selectedMetrics.forEach((metric) => { 35 switch (metric) { 36 case 'revenue': 37 dataPoint[metric] = items.reduce( 38 (sum, item) => sum + item.revenue, 39 0 40 ); 41 break; 42 case 'users': 43 dataPoint[metric] = new Set( 44 items.map((item) => item.userId) 45 ).size; 46 break; 47 case 'conversions': 48 dataPoint[metric] = items.filter((item) => item.converted).length; 49 break; 50 } 51 }); 52 53 return dataPoint; 54 }) 55 .sort((a, b) => new Date(a.period) - new Date(b.period)); 56 }, [rawData, dateRange, groupBy, selectedMetrics]); 57 58 // Chart configuration object ๐ 59 const chartConfig = useMemo( 60 () => ({ 61 type: chartType, 62 data: processedData, 63 options: { 64 responsive: true, 65 maintainAspectRatio: false, 66 plugins: { 67 title: { 68 display: true, 69 text: `๐ Analytics Dashboard - ${groupBy.charAt(0).toUpperCase() + groupBy.slice(1)} View`, 70 }, 71 legend: { 72 display: selectedMetrics.length > 1, 73 }, 74 }, 75 scales: { 76 y: { 77 beginAtZero: true, 78 ticks: { 79 callback: function (value) { 80 return selectedMetrics.includes('revenue') 81 ? `๐ฐ $${value}` 82 : value; 83 }, 84 }, 85 }, 86 }, 87 }, 88 }), 89 [chartType, processedData, groupBy, selectedMetrics] 90 ); 91 92 // Event handlers with useCallback ๐ฏ 93 const handleDateRangeChange = useCallback((newRange) => { 94 setDateRange(newRange); 95 }, []); 96 97 const handleMetricToggle = useCallback((metric) => { 98 setSelectedMetrics((prev) => 99 prev.includes(metric) 100 ? prev.filter((m) => m !== metric) 101 : [...prev, metric] 102 ); 103 }, []); 104 105 const handleExport = useCallback(() => { 106 const csvContent = convertToCSV(processedData); 107 const blob = new Blob([csvContent], { type: 'text/csv' }); 108 const url = URL.createObjectURL(blob); 109 const link = document.createElement('a'); 110 link.href = url; 111 link.download = `๐ analytics-${groupBy}-${Date.now()}.csv`; 112 link.click(); 113 URL.revokeObjectURL(url); 114 }, [processedData, groupBy]); 115 116 return ( 117 <div className="dashboard"> 118 <div className="controls"> 119 <DateRangePicker value={dateRange} onChange={handleDateRangeChange} /> 120 <GroupBySelector value={groupBy} onChange={setGroupBy} /> 121 <MetricSelector 122 selectedMetrics={selectedMetrics} 123 onToggle={handleMetricToggle} 124 /> 125 <ChartTypeSelector value={chartType} onChange={setChartType} /> 126 </div> 127 128 <div className="chart-container"> 129 <Chart config={chartConfig} /> 130 </div> 131 132 <div className="actions"> 133 <button onClick={handleExport}>๐ฅ Export Data</button> 134 </div> 135 </div> 136 ); 137}; 138 139// Utility function for date grouping ๐ 140const getGroupKey = (timestamp, groupBy) => { 141 const date = new Date(timestamp); 142 switch (groupBy) { 143 case 'day': 144 return date.toISOString().split('T')[0]; 145 case 'week': 146 const weekStart = new Date(date.setDate(date.getDate() - date.getDay())); 147 return weekStart.toISOString().split('T')[0]; 148 case 'month': 149 return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`; 150 default: 151 return timestamp; 152 } 153}; 154 155const convertToCSV = (data) => { 156 if (!data.length) return ''; 157 158 const headers = Object.keys(data[0]); 159 const csvRows = [ 160 headers.join(','), 161 ...data.map((row) => headers.map((header) => row[header]).join(',')), 162 ]; 163 164 return csvRows.join('\n'); 165};
jsx1// โ Bad - Unnecessary memoization 2const UserGreeting = ({ user }) => { 3 const greeting = useMemo(() => `๐ Hello, ${user.name}!`, [user.name]); 4 const isLoggedIn = useMemo(() => !!user, [user]); 5 6 return ( 7 <div> 8 {greeting} {isLoggedIn && '๐ Welcome back!'} 9 </div> 10 ); 11}; 12 13// โ Good - Simple calculations don't need memoization 14const UserGreeting = ({ user }) => { 15 const greeting = `๐ Hello, ${user.name}!`; 16 const isLoggedIn = !!user; 17 18 return ( 19 <div> 20 {greeting} {isLoggedIn && '๐ Welcome back!'} 21 </div> 22 ); 23};
jsx1// โ Bad - Missing dependencies 2const SearchResults = ({ query, filters }) => { 3 const results = useMemo(() => { 4 return searchData(query, filters); 5 }, [query]); // Missing 'filters'! ๐จ 6 7 return <ResultList results={results} />; 8}; 9 10// โ Good - All dependencies included 11const SearchResults = ({ query, filters }) => { 12 const results = useMemo(() => { 13 return searchData(query, filters); 14 }, [query, filters]); // All dependencies included โ 15 16 return <ResultList results={results} />; 17};
jsx1// โ Bad - Object created in render 2const ProductList = ({ products }) => { 3 const sortConfig = { field: 'name', direction: 'asc' }; 4 5 const sortedProducts = useMemo(() => { 6 return sortProducts(products, sortConfig); 7 }, [products, sortConfig]); // sortConfig is always new! ๐จ 8 9 return <div>{/* render products */}</div>; 10}; 11 12// โ Good - Stable dependencies 13const ProductList = ({ products }) => { 14 const sortedProducts = useMemo(() => { 15 const sortConfig = { field: 'name', direction: 'asc' }; 16 return sortProducts(products, sortConfig); 17 }, [products]); // Stable dependencies โ 18 19 return <div>{/* render products */}</div>; 20};
Before optimizing, always measure performance:
Paint flashing highlights the areas of the webpage that the browser engine repaints, making it possible for you to visually identify the problematic areas:
How to enable:
What to look for:
jsx1const useBenchmark = (name, fn, deps) => { 2 return useMemo(() => { 3 const start = performance.now(); 4 const result = fn(); 5 const end = performance.now(); 6 7 if (end - start > 1) { 8 // Only log if > 1ms โฑ๏ธ 9 console.log(`โก ${name}: ${(end - start).toFixed(2)}ms`); 10 } 11 12 return result; 13 }, deps); 14}; 15 16// Usage 17const DataProcessor = ({ data }) => { 18 const processedData = useBenchmark( 19 'Data Processing', 20 () => expensiveDataProcessing(data), 21 [data] 22 ); 23 24 return <DataDisplay data={processedData} />; 25};
Scrolling Performance Issues:
Before applying these hooks, ask yourself:
useMemo
and useCallback
are powerful optimization tools when used correctly. They solve specific problems related to expensive calculations and unnecessary re-renders. The key is understanding when these problems actually exist and applying the right solution.
Remember: React is already highly optimized. These hooks should be used to solve measured performance issues, not applied everywhere preemptively. Start with clean component architecture, measure performance with proper tools, and then optimize the bottlenecks you actually find.
Use the examples and patterns in this guide as templates, but always adapt them to your specific use case and measure the results to ensure you're actually improving performance rather than just adding complexity.
Happy optimizing! ๐โจ