How to Mock useTransition Hook in React

Testing React components that utilize concurrent features can sometimes be challenging due to the asynchronous nature of state transitions. This article provides a straightforward guide on how to mock the useTransition hook in React using Jest and React Testing Library. You will learn how to mock the hook’s return values to control the pending state and ensure your transition callbacks execute synchronously during unit tests.

Why Mock useTransition?

The useTransition hook returns an array containing two elements: a boolean flag (isPending) and a function to start the transition (startTransition).

In a testing environment, the asynchronous scheduling of concurrent rendering can lead to flaky tests or race conditions. Mocking useTransition allows you to: * Force the transition callback to execute synchronously. * Explicitly control the isPending state to test loading UI states. * Isolate your component’s business logic from React’s internal concurrent scheduler.

Standard Synchronous Mocking

To mock useTransition so that it executes the transition callback immediately, you can spy on the react module and provide a custom implementation. This is the most common approach for general testing.

Add the following mock setup at the top of your test file:

import React from 'react';

jest.spyOn(React, 'useTransition').mockReturnValue([
  false, // isPending is false
  (callback) => callback(), // startTransition executes the callback immediately
]);

By immediately invoking the callback passed to the mock startTransition function, your state updates execute synchronously, making them easy to assert using standard React Testing Library utilities.

Testing the Pending State

If you need to test how your UI behaves when a transition is actively pending (i.e., when isPending is true), you can temporarily override the mock return value within a specific test block.

test('renders loading state during transition', () => {
  // Mock useTransition to return isPending as true
  jest.spyOn(React, 'useTransition').mockReturnValue([
    true, 
    (callback) => callback()
  ]);

  render(<MyComponent />);
  
  // Assert that your loading spinner or pending UI is visible
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Complete Example

Below is a complete implementation showing how to test a component that uses useTransition to filter a list.

The Component (FilterList.js)

import React, { useState, useTransition } from 'react';

export function FilterList({ items }) {
  const [isPending, startTransition] = useTransition();
  const [filterTerm, setFilterTerm] = useState('');

  const handleChange = (e) => {
    startTransition(() => {
      setFilterTerm(e.target.value);
    });
  };

  const filteredItems = items.filter(item => item.includes(filterTerm));

  return (
    <div>
      <input type="text" onChange={handleChange} placeholder="Search..." />
      {isPending && <p>Updating list...</p>}
      <ul>
        {filteredItems.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

The Test Suite (FilterList.test.js)

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

describe('FilterList Component', () => {
  beforeEach(() => {
    jest.restoreAllMocks();
  });

  test('updates filter term synchronously when useTransition is mocked', () => {
    // Mock useTransition to run synchronously
    jest.spyOn(React, 'useTransition').mockReturnValue([
      false,
      (callback) => callback(),
    ]);

    const items = ['Apple', 'Banana', 'Cherry'];
    render(<FilterList items={items} />);

    const input = screen.getByPlaceholderText('Search...');
    fireEvent.change(input, { target: { value: 'Banana' } });

    // Assert that the list filtered immediately
    expect(screen.getByText('Banana')).toBeInTheDocument();
    expect(screen.queryByText('Apple')).not.toBeInTheDocument();
  });
});