Javascript
Front End
React.js
hooks
performance
Performance optimization is an important consideration in React applications, especially when they grow in complexity. The useMemo
hook is a powerful tool that lets you memoize expensive calculations to prevent unnecessary re-computations during renders.
useMemo
is a React hook that memoizes the result of a function call. It only recomputes the memoized value when one of its dependencies has changed, which can significantly improve performance by avoiding expensive calculations on every render.
1const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
In this syntax:
useEffect
)You should consider using useMemo
when:
1import React, { useState, useMemo } from 'react'; 2 3function FactorialCalculator() { 4 const [number, setNumber] = useState(1); 5 const [otherState, setOtherState] = useState(false); 6 7 // This calculation will only run when 'number' changes 8 const factorial = useMemo(() => { 9 console.log('Calculating factorial...'); 10 // Expensive calculation 11 let result = 1; 12 for (let i = 1; i <= number; i++) { 13 result *= i; 14 } 15 return result; 16 }, [number]); // Only recalculate when number changes 17 18 return ( 19 <div> 20 <input 21 type="number" 22 value={number} 23 onChange={e => setNumber(parseInt(e.target.value))} 24 /> 25 <p>Factorial of {number} is: {factorial}</p> 26 <button onClick={() => setOtherState(!otherState)}> 27 Toggle other state 28 </button> 29 </div> 30 ); 31}
In this example:
useMemo
to avoid recalculating it on every rendernumber
state changesotherState
changes, the component re-renders but doesn't recalculate the factorialTo understand the benefit of useMemo
, let's compare it with a regular variable approach:
1function WithoutMemo({ list, filter }) { 2 // This runs on EVERY render 3 const filteredList = list.filter(item => item.includes(filter)); 4 5 return ( 6 <div> 7 <p>Found {filteredList.length} items</p> 8 <ul> 9 {filteredList.map(item => <li key={item}>{item}</li>)} 10 </ul> 11 </div> 12 ); 13}
1function WithMemo({ list, filter }) { 2 // This only runs when list or filter changes 3 const filteredList = useMemo(() => { 4 return list.filter(item => item.includes(filter)); 5 }, [list, filter]); 6 7 return ( 8 <div> 9 <p>Found {filteredList.length} items</p> 10 <ul> 11 {filteredList.map(item => <li key={item}>{item}</li>)} 12 </ul> 13 </div> 14 ); 15}
1function ProductList({ products, searchTerm, sortOrder }) { 2 const filteredAndSortedProducts = useMemo(() => { 3 // First filter the products 4 const filtered = searchTerm 5 ? products.filter(product => 6 product.name.toLowerCase().includes(searchTerm.toLowerCase()) 7 ) 8 : products; 9 10 // Then sort them 11 return [...filtered].sort((a, b) => { 12 if (sortOrder === 'price') { 13 return a.price - b.price; 14 } else if (sortOrder === 'name') { 15 return a.name.localeCompare(b.name); 16 } 17 return 0; 18 }); 19 }, [products, searchTerm, sortOrder]); 20 21 return ( 22 <div> 23 {filteredAndSortedProducts.map(product => ( 24 <ProductItem key={product.id} product={product} /> 25 ))} 26 </div> 27 ); 28}
1function ChartComponent({ data, width, height }) { 2 // Memoize options object to prevent unnecessary re-rendering 3 const chartOptions = useMemo(() => { 4 return { 5 data, 6 dimensions: { 7 width, 8 height 9 }, 10 margin: { top: 20, right: 30, bottom: 40, left: 50 }, 11 animate: true, 12 colors: generateColorPalette(), // Expensive function 13 legend: buildLegendConfig() // Another expensive function 14 }; 15 }, [data, width, height]); 16 17 return <Chart options={chartOptions} />; 18}
1function DataTable({ rawData }) { 2 const processedData = useMemo(() => { 3 // Complex data transformation 4 return rawData.map(item => ({ 5 ...item, 6 fullName: `${item.firstName} ${item.lastName}`, 7 formattedDate: formatDate(item.timestamp), 8 calculatedValue: calculateValue(item.values) 9 })); 10 }, [rawData]); 11 12 return ( 13 <table> 14 <thead> 15 <tr> 16 <th>Name</th> 17 <th>Date</th> 18 <th>Value</th> 19 </tr> 20 </thead> 21 <tbody> 22 {processedData.map(item => ( 23 <tr key={item.id}> 24 <td>{item.fullName}</td> 25 <td>{item.formattedDate}</td> 26 <td>{item.calculatedValue}</td> 27 </tr> 28 ))} 29 </tbody> 30 </table> 31 ); 32}
useMemo
works well with React.memo
for optimizing both computation and rendering:
1// Child component wrapped in React.memo 2const ExpensiveComponent = React.memo(({ data }) => { 3 // Expensive rendering 4 return <div>{/* Render data */}</div>; 5}); 6 7function ParentComponent() { 8 const [count, setCount] = useState(0); 9 const [items, setItems] = useState([1, 2, 3]); 10 11 // This object is memoized and only recreated when items changes 12 const memoizedData = useMemo(() => { 13 return { 14 items, 15 meta: { 16 count: items.length, 17 modified: new Date() 18 } 19 }; 20 }, [items]); 21 22 return ( 23 <div> 24 <button onClick={() => setCount(count + 1)}> 25 Increment counter: {count} 26 </button> 27 28 {/* 29 Without useMemo, a new data object would be created on every render, 30 causing ExpensiveComponent to re-render even when items hasn't changed 31 */} 32 <ExpensiveComponent data={memoizedData} /> 33 </div> 34 ); 35}
While useMemo
can improve performance, it also has costs:
Don't use useMemo
for simple calculations where the memoization overhead might be more expensive than just recalculating the value:
1// DON'T DO THIS - Overhead isn't worth it 2const doubledValue = useMemo(() => value * 2, [value]); 3 4// DO THIS INSTEAD 5const doubledValue = value * 2;
Also avoid using it for every calculation in your component by default. Only add memoization when you've identified a specific performance problem.
Always measure performance before and after adding useMemo
to ensure it's actually helping:
1// You can use console.time to measure 2function Component({ data }) { 3 const processedData = useMemo(() => { 4 console.time('data processing'); 5 const result = expensiveOperation(data); 6 console.timeEnd('data processing'); 7 return result; 8 }, [data]); 9 10 // Rest of component 11}
Just like with useEffect
, include all values from the component scope that are used inside the calculation function:
1function SearchResults({ query, maxResults, data }) { 2 const results = useMemo(() => { 3 // All three values are used, so all three should be dependencies 4 return data 5 .filter(item => item.name.includes(query)) 6 .slice(0, maxResults); 7 }, [data, query, maxResults]); // All dependencies listed 8 9 return <ResultsList results={results} />; 10}
If your memoized calculation depends on functions, consider memoizing those functions with useCallback
:
1function FilterableList({ items }) { 2 const [filterText, setFilterText] = useState(''); 3 4 // Memoize the filter function 5 const filterFunction = useCallback( 6 (item) => item.name.includes(filterText), 7 [filterText] 8 ); 9 10 // Use the memoized function in useMemo 11 const filteredItems = useMemo(() => { 12 return items.filter(filterFunction); 13 }, [items, filterFunction]); 14 15 // Rest of component 16}
1import React, { useState, useMemo } from 'react'; 2import Chart from './Chart'; 3import DataTable from './DataTable'; 4 5function Dashboard({ rawData }) { 6 const [timeRange, setTimeRange] = useState('week'); 7 const [metric, setMetric] = useState('revenue'); 8 9 // Memoize time filtering logic 10 const timeFilteredData = useMemo(() => { 11 const now = new Date(); 12 const timeRanges = { 13 day: 24 * 60 * 60 * 1000, 14 week: 7 * 24 * 60 * 60 * 1000, 15 month: 30 * 24 * 60 * 60 * 1000 16 }; 17 18 const threshold = now.getTime() - timeRanges[timeRange]; 19 20 return rawData.filter(item => 21 new Date(item.timestamp).getTime() > threshold 22 ); 23 }, [rawData, timeRange]); 24 25 // Memoize metric calculations 26 const metricData = useMemo(() => { 27 // Group by day 28 const grouped = timeFilteredData.reduce((acc, item) => { 29 const date = new Date(item.timestamp).toLocaleDateString(); 30 if (!acc[date]) { 31 acc[date] = { date, value: 0 }; 32 } 33 acc[date].value += item[metric]; 34 return acc; 35 }, {}); 36 37 // Convert to array and sort 38 return Object.values(grouped).sort((a, b) => 39 new Date(a.date) - new Date(b.date) 40 ); 41 }, [timeFilteredData, metric]); 42 43 // Memoize statistics 44 const stats = useMemo(() => { 45 if (metricData.length === 0) return { total: 0, avg: 0, max: 0 }; 46 47 const total = metricData.reduce((sum, item) => sum + item.value, 0); 48 return { 49 total, 50 avg: total / metricData.length, 51 max: Math.max(...metricData.map(item => item.value)) 52 }; 53 }, [metricData]); 54 55 return ( 56 <div className="dashboard"> 57 <div className="controls"> 58 <select 59 value={timeRange} 60 onChange={e => setTimeRange(e.target.value)} 61 > 62 <option value="day">Last 24 Hours</option> 63 <option value="week">Last 7 Days</option> 64 <option value="month">Last 30 Days</option> 65 </select> 66 67 <select 68 value={metric} 69 onChange={e => setMetric(e.target.value)} 70 > 71 <option value="revenue">Revenue</option> 72 <option value="users">Users</option> 73 <option value="pageviews">Pageviews</option> 74 </select> 75 </div> 76 77 <div className="summary"> 78 <div className="stat"> 79 <h3>Total {metric}</h3> 80 <p>{stats.total.toLocaleString()}</p> 81 </div> 82 <div className="stat"> 83 <h3>Average {metric}</h3> 84 <p>{stats.avg.toLocaleString(undefined, { maximumFractionDigits: 2 })}</p> 85 </div> 86 <div className="stat"> 87 <h3>Maximum {metric}</h3> 88 <p>{stats.max.toLocaleString()}</p> 89 </div> 90 </div> 91 92 <Chart data={metricData} /> 93 94 <DataTable data={timeFilteredData} /> 95 </div> 96 ); 97}
This complex example demonstrates several layers of useMemo
working together:
The useMemo
hook is a powerful tool for optimizing the performance of your React applications by avoiding unnecessary recalculations. By memoizing expensive computations, you can ensure that they only run when their inputs actually change, not on every render.
Key takeaways:
useMemo
for expensive calculations that depend on state or propsuseMemo
for simple calculationsReact.memo
for complete rendering optimizationRemember that premature optimization can lead to more complex, harder-to-maintain code. Use useMemo
when you've identified a specific performance issue, not as a default approach for all calculations in your components.