Questions
This component performs expensive calculations unnecessarily. Optimize it with `useMemo`.
The Scenario
A developer has a ProductList component that displays a list of products. It also calculates a totalPrice from the list.
They’ve noticed a performance issue: the totalPrice is re-calculated on every render of the ProductList, even when the products array itself hasn’t changed (e.g., when a parent component re-renders due to an unrelated state change). This expensive calculation is causing the UI to feel sluggish.
The Challenge
You’ve been given the App and ProductList components.
- Explain why the
totalPricecalculation is happening unnecessarily on every render. - Fix the
ProductListcomponent using theuseMemohook to prevent redundant calculations, ensuringtotalPriceis only re-calculated when theproductsarray actually changes.
import React, { useState } from 'react';
// Simulate an expensive calculation
const calculateTotalPrice = (products) => {
console.log('Calculating total price...'); // This log indicates an expensive operation
let total = 0;
for (let i = 0; i < 10000000; i++) { // Simulate heavy computation
total += Math.random();
}
return products.reduce((sum, product) => sum + product.price, 0);
};
function ProductList({ products }) {
// THE BUG: This expensive calculation runs on every render,
// even if 'products' hasn't changed.
const totalPrice = calculateTotalPrice(products);
return (
<div style={{ border: '1px solid blue', padding: '10px', margin: '10px' }}>
<h2>Product List</h2>
<ul>
{products.map(product => (
<li key={product.id}>{product.name} - ${product.price.toFixed(2)}</li>
))}
</ul>
<h3>Total Price: ${totalPrice.toFixed(2)}</h3>
</div>
);
}
export default function App() {
const [count, setCount] = useState(0); // State to force App re-renders
const products = [
{ id: 1, name: 'Laptop', price: 1200.00 },
{ id: 2, name: 'Mouse', price: 25.00 },
{ id: 3, name: 'Keyboard', price: 75.00 },
];
return (
<div style={{ padding: '20px' }}>
<h1>App Component</h1>
<button onClick={() => setCount(c => c + 1)}>
Force App Re-render (Count: {count})
</button>
<hr />
<ProductList products={products} />
</div>
);
} The Explanation: Expensive Calculations and Re-renders
[object Object]
[object Object]
Why the Problem Occurs
In React functional components, the entire component function body is re-executed on every render. If you have a computationally expensive operation (like calculateTotalPrice) directly inside your component, it will run every time the component re-renders, regardless of whether the inputs to that calculation have actually changed.
In our scenario:
- The
Appcomponent re-renders when itscountstate changes. Appthen re-rendersProductList.- Inside
ProductList,calculateTotalPrice(products)is called again, even though theproductsarray (which is a constant inApp) hasn’t changed its reference or content. This leads to the unnecessary re-calculation.
The Fix: Memoizing Values with useMemo
The useMemo hook is designed to optimize performance by memoizing the result of an expensive calculation. It takes two arguments:
- A function that performs the calculation.
- A dependency array.
useMemo will only re-run the calculation function and return a new value if any of the dependencies in the array have changed. Otherwise, it returns the previously memoized value.
Here is the corrected implementation:
import React, { useState, useMemo } from 'react';
const calculateTotalPrice = (products) => {
console.log('Calculating total price...');
let total = 0;
for (let i = 0; i < 10000000; i++) {
total += Math.random();
}
return products.reduce((sum, product) => sum + product.price, 0);
};
function ProductList({ products }) {
// FIX: Memoize the totalPrice calculation using useMemo.
// It will only re-run if the 'products' array reference changes.
const totalPrice = useMemo(() => calculateTotalPrice(products), [products]);
return (
<div style={{ border: '1px solid blue', padding: '10px', margin: '10px' }}>
<h2>Product List</h2>
<ul>
{products.map(product => (
<li key={product.id}>{product.name} - ${product.price.toFixed(2)}</li>
))}
</ul>
<h3>Total Price: ${totalPrice.toFixed(2)}</h3>
</div>
);
}
export default function App() {
const [count, setCount] = useState(0);
// The 'products' array is a constant, so its reference never changes.
const products = [
{ id: 1, name: 'Laptop', price: 1200.00 },
{ id: 2, name: 'Mouse', price: 25.00 },
{ id: 3, name: 'Keyboard', price: 75.00 },
];
return (
<div style={{ padding: '20px' }}>
<h1>App Component</h1>
<button onClick={() => setCount(c => c + 1)}>
Force App Re-render (Count: {count})
</button>
<hr />
<ProductList products={products} />
</div>
);
}
With useMemo, the calculateTotalPrice function will only be executed when the products array (its dependency) changes. Since the products array in App is a constant and its reference never changes, totalPrice will only be calculated once on the initial render, preventing redundant expensive computations.
Practice Question
What is the primary purpose of the `useMemo` hook?