React - useMemo: Optimizing Calculations

When your component performs expensive calculations on every render, useMemo will memorize the result and only recalculate it if the dependencies change.


The Problem of Unnecessary Recalculations

Expensive Calculations on Every Render

function SlowComponent({ items, filter }) {
  const [count, setCount] = useState(0)

  // ❌ PROBLEM - Expensive calculation on EVERY render!
  const expensiveValue = items
    .filter(item => item.category === filter)
    .reduce((sum, item) => sum + item.price, 0)
  
  console.log('💰 Expensive calculation executed!') // ← Triggers even when count changes!

  // ❌ PROBLEM - New object on every render!
  const chartConfig = {
    type: 'bar',
    data: expensiveValue,
    options: { responsive: true }
  }

  // ❌ PROBLEM - Array recalculated on every render!
  const sortedItems = items
    .filter(item => item.category === filter)
    .sort((a, b) => b.price - a.price)

  return (
    <div>
      <h3>Total: {expensiveValue}€</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      <ExpensiveChart config={chartConfig} />
      <ItemsList items={sortedItems} />
    </div>
  )
}

// These components re-render even if their props haven't really changed!
const ExpensiveChart = memo(function ExpensiveChart({ config }) {
  console.log('📊 ExpensiveChart re-rendered') // ← Always called because config is always a new object!
  return <div>Chart with config: {JSON.stringify(config)}</div>
})

const ItemsList = memo(function ItemsList({ items }) {
  console.log('📝 ItemsList re-rendered') // ← Always called because items is always a new array!
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name} - {item.price}€</li>
      ))}
    </ul>
  )
})

useMemo to the Rescue

Syntax and Basic Usage

import { useMemo } from 'react'

function OptimizedComponent({ items, filter }) {
  const [count, setCount] = useState(0)

  // ✅ SOLUTION - Calculation only if items or filter change
  const expensiveValue = useMemo(() => {
    console.log('💰 Expensive calculation executed') // ← Only if necessary!
    
    return items
      .filter(item => item.category === filter)
      .reduce((sum, item) => sum + item.price, 0)
  }, [items, filter]) // ← Dependencies

  // ✅ Complex object memorized
  const chartConfig = useMemo(() => {
    console.log('📊 Chart configuration generated')
    
    return {
      type: 'bar',
      data: expensiveValue,
      options: { responsive: true },
      theme: 'modern'
    }
  }, [expensiveValue])

  // ✅ Filtered and sorted data memorized
  const sortedItems = useMemo(() => {
    console.log('🔍 Filtering and sorting items')
    
    return items
      .filter(item => item.category === filter)
      .sort((a, b) => b.price - a.price) // Sorting by descending price
  }, [items, filter])

  return (
    <div>
      <h3>Total: {expensiveValue}€</h3>
      <p>Count: {count} (no longer triggers the calculation!)</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      <ExpensiveChart config={chartConfig} />
      <ItemsList items={sortedItems} />
    </div>
  )
}

// Now these components only re-render if their props really change!
const ExpensiveChart = memo(function ExpensiveChart({ config }) {
  console.log('📊 ExpensiveChart re-rendered') // ← Only if config really changes
  return <div>Chart with config: {JSON.stringify(config)}</div>
})

const ItemsList = memo(function ItemsList({ items }) {
  console.log('📝 ItemsList re-rendered') // ← Only if items really change
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name} - {item.price}€</li>
      ))}
    </ul>
  )
})

Resources For Further Exploration