DeployU
Interviews / Frontend Engineering / This component performs expensive calculations unnecessarily. Optimize it with `useMemo`.

Questions

This component performs expensive calculations unnecessarily. Optimize it with `useMemo`.

practical Performance Interactive Quiz Code Examples

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.

  1. Explain why the totalPrice calculation is happening unnecessarily on every render.
  2. Fix the ProductList component using the useMemo hook to prevent redundant calculations, ensuring totalPrice is only re-calculated when the products array actually changes.
Your Code
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>
  );
}
Click "Run Code" to see output...
Click "Run Code" to see test results...

The Explanation: Expensive Calculations and Re-renders

Wrong Approach

[object Object]

Right Approach

[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:

  1. The App component re-renders when its count state changes.
  2. App then re-renders ProductList.
  3. Inside ProductList, calculateTotalPrice(products) is called again, even though the products array (which is a constant in App) 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:

  1. A function that performs the calculation.
  2. 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?