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
});
});