How to Test useMemo Hook in React

Testing the useMemo hook in React ensures that expensive computations are properly cached and only recalculated when their dependencies change. This article guides you through the best practices for testing useMemo using React Testing Library and Jest, demonstrating how to verify both component behavior and dependency-based re-evaluation without testing implementation details.


The Core Strategy: Test Behavior, Not Internals

When testing React components that use useMemo, you should avoid testing the Hook directly. Instead, focus on verifying the output and performance characteristics. You want to prove two things: 1. The component renders the correct computed value. 2. The expensive calculation function is not called again if the dependencies remain the same, but is called again when they change.


Method 1: Testing useMemo within a Component

The most common way to test useMemo is by rendering the component that consumes it. To verify that memoization is working, you can pass a mocked calculation function as a prop and assert how many times it gets called.

1. The Component Under Test

import React, { useMemo } from 'react';

export function HeavyCalculationComponent({ value, unrelatedProp, computeFn }) {
  // useMemo caches the result of the computeFn
  const memoizedValue = useMemo(() => computeFn(value), [value, computeFn]);

  return (
    <div>
      <span data-testid="result">{memoizedValue}</span>
      <span data-testid="unrelated">{unrelatedProp}</span>
    </div>
  );
}

2. The Test Case

Using React Testing Library and Jest, you can render the component, update unrelated props to trigger re-renders, and verify the computation function’s call count.

import React from 'react';
import { render, screen } from '@testing-library/react';
import { HeavyCalculationComponent } from './HeavyCalculationComponent';

describe('HeavyCalculationComponent', () => {
  it('should memoize the calculation and only re-run when dependencies change', () => {
    const mockComputeFn = jest.fn((val) => val * 2);

    // Initial Render
    const { rerender } = render(
      <HeavyCalculationComponent 
        value={5} 
        unrelatedProp="A" 
        computeFn={mockComputeFn} 
      />
    );

    expect(screen.getByTestId('result').textContent).toBe('10');
    expect(mockComputeFn).toHaveBeenCalledTimes(1);

    // Rerender with an unrelated prop changed (value stays the same)
    rerender(
      <HeavyCalculationComponent 
        value={5} 
        unrelatedProp="B" 
        computeFn={mockComputeFn} 
      />
    );

    // The result should be the same, and the mock function should NOT be called again
    expect(screen.getByTestId('result').textContent).toBe('10');
    expect(mockComputeFn).toHaveBeenCalledTimes(1);

    // Rerender with the dependency changed
    rerender(
      <HeavyCalculationComponent 
        value={10} 
        unrelatedProp="B" 
        computeFn={mockComputeFn} 
      />
    );

    // The result should update, and the mock function should be called a second time
    expect(screen.getByTestId('result').textContent).toBe('20');
    expect(mockComputeFn).toHaveBeenCalledTimes(2);
  });
});

Method 2: Testing useMemo inside a Custom Hook

If your useMemo logic is isolated inside a custom hook, you can test it directly using the renderHook utility from @testing-library/react.

1. The Custom Hook

import { useMemo } from 'react';

export function useDoubleValue(number, computeFn) {
  return useMemo(() => computeFn(number), [number, computeFn]);
}

2. The Test Case

import { renderHook } from '@testing-library/react';
import { useDoubleValue } from './useDoubleValue';

describe('useDoubleValue Custom Hook', () => {
  it('should only recalculate when the number dependency changes', () => {
    const mockComputeFn = jest.fn((val) => val * 2);

    const { result, rerender } = renderHook(
      ({ number }) => useDoubleValue(number, mockComputeFn),
      { initialProps: { number: 5 } }
    );

    expect(result.current).toBe(10);
    expect(mockComputeFn).toHaveBeenCalledTimes(1);

    // Rerender with the same number value
    rerender({ number: 5 });
    expect(result.current).toBe(10);
    expect(mockComputeFn).toHaveBeenCalledTimes(1); // Calculation was skipped

    // Rerender with a new number value
    rerender({ number: 20 });
    expect(result.current).toBe(40);
    expect(mockComputeFn).toHaveBeenCalledTimes(2); // Recalculation triggered
  });
});