Back to Blog

Optimizing React Performance with useMemo and useCallback

Published on May 8, 2025

React's useMemo and useCallback hooks are powerful tools for optimizing performance, but they're often misused. Let's explore when and how to use them effectively.

Understanding the Problem

React re-renders components when their props or state change. Sometimes, expensive calculations or function recreations can cause performance bottlenecks.

useMemo: Memoizing Expensive Calculations

useMemo is used to memoize the result of expensive calculations:

import { useMemo, useState } from 'react';

function ExpensiveComponent({ items }) {
  const [filter, setFilter] = useState('');

  // Without useMemo, this calculation runs on every render
  const expensiveValue = useMemo(() => {
    console.log('Calculating expensive value...');
    return items
      .filter(item => item.name.includes(filter))
      .reduce((sum, item) => sum + item.value, 0);
  }, [items, filter]);

  return (
    <div>
      <input 
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter items..."
      />
      <p>Total value: {expensiveValue}</p>
    </div>
  );
}

useCallback: Memoizing Functions

useCallback is used to memoize functions, preventing unnecessary re-renders of child components:

import { useCallback, useState, memo } from 'react';

// Child component wrapped in memo
const ChildComponent = memo(({ onClick, title }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>{title}</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // Without useCallback, this function is recreated on every render
  const handleClick = useCallback(() => {
    console.log('Button clicked!');
  }, []); // Empty dependency array means this function never changes

  const incrementCount = useCallback(() => {
    setCount(prev => prev + 1);
  }, []); // This is safe because we use the updater function

  return (
    <div>
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name..."
      />
      <p>Count: {count}</p>
      <ChildComponent onClick={handleClick} title="Click me" />
      <ChildComponent onClick={incrementCount} title="Increment" />
    </div>
  );
}

Common Mistakes to Avoid

1. Overusing useMemo and useCallback

Don't wrap everything in useMemo or useCallback. They have their own overhead:

// ❌ Bad: Unnecessary memoization
const simpleValue = useMemo(() => props.value * 2, [props.value]);

// ✅ Good: Simple calculation, no memoization needed
const simpleValue = props.value * 2;

2. Wrong Dependencies

Always include all dependencies in the dependency array:

// ❌ Bad: Missing dependency
const calculateTotal = useCallback(() => {
  return items.reduce((sum, item) => sum + item.price * taxRate, 0);
}, [items]); // Missing taxRate!

// ✅ Good: All dependencies included
const calculateTotal = useCallback(() => {
  return items.reduce((sum, item) => sum + item.price * taxRate, 0);
}, [items, taxRate]);

When to Use These Hooks

Use useMemo when:

  • You have expensive calculations that don't need to run on every render
  • You're creating objects or arrays that are passed as props to memoized components
  • You're breaking referential equality unnecessarily

Use useCallback when:

  • You're passing functions as props to memoized child components
  • The function is a dependency of other hooks
  • You're creating event handlers in loops

Measuring Performance

Always measure the actual performance impact:

import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration) {
  console.log('Component:', id, 'Phase:', phase, 'Duration:', actualDuration);
}

function App() {
  return (
    <Profiler id="ExpensiveComponent" onRender={onRenderCallback}>
      <ExpensiveComponent />
    </Profiler>
  );
}

Alternative: React.memo

Sometimes React.memo is all you need:

const OptimizedComponent = memo(function MyComponent({ title, onClick }) {
  return <button onClick={onClick}>{title}</button>;
});

// Or with custom comparison
const OptimizedComponent = memo(function MyComponent(props) {
  return <div>{/* component content */}</div>;
}, (prevProps, nextProps) => {
  // Return true if props are equal (skip re-render)
  return prevProps.title === nextProps.title;
});

Remember: premature optimization is the root of all evil. Use these hooks when you have identified actual performance problems, not preemptively.