How to Mock React Reconciliation

React’s reconciliation algorithm is responsible for diffing the virtual DOM and updating the actual DOM. While you rarely need to mock the diffing algorithm itself, testing asynchronous updates, concurrent features, or complex state transitions requires control over when and how reconciliation runs. This article covers how to mock and control React reconciliation in your test environment using Jest, act(), and the mock scheduler.

Why You Mock Reconciliation Effects, Not the Algorithm

Directly mocking the internal reconciliation engine (react-reconciler) is highly discouraged because it is an internal implementation detail subject to change. Instead, mocking reconciliation means controlling the timing of state updates and rendering queues.

In testing, you achieve this by: 1. Flushing updates synchronously using act(). 2. Mocking the React Scheduler to control when concurrent updates are processed. 3. Mocking Custom Renderers if you are testing non-DOM environments (like Canvas or WebGL).


Method 1: Using act() to Control Reconciliation

The most common way to mock or force a reconciliation cycle in unit tests is by wrapping state-changing operations in act(). This tells React to process all queued updates and complete reconciliation before executing the next line of code.

import { render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import MyComponent from './MyComponent';

test('flushes reconciliation cycle', () => {
  render(<MyComponent />);
  
  const button = screen.getByRole('button');
  
  // act() forces the reconciliation cycle to run and complete
  act(() => {
    button.click();
  });

  expect(screen.getByText('Updated State')).toBeInTheDocument();
});

Method 2: Mocking the React Scheduler

For advanced testing—especially when dealing with React 18’s concurrent features like useTransition or useDeferredValue—you need to control the scheduling of reconciliation tasks. React uses the scheduler package internally to prioritize updates.

You can mock this scheduler to manually step through reconciliation phases.

1. Configure the Jest Mock

Create a mock for the React scheduler using the official scheduler/unstable_mock package.

// jest.config.js or setupTests.js
jest.mock('scheduler', () => require('scheduler/unstable_mock'));

2. Control Reconciliation in Your Test

Using the mocked scheduler, you can assert on what is scheduled, flush pending jobs up to a certain point, and control the virtual passage of time.

import * as React from 'react';
import { render } from '@testing-library/react';
import Scheduler from 'scheduler/unstable_mock';
import ConcurrentComponent from './ConcurrentComponent';

test('manually steps through concurrent reconciliation', () => {
  render(<ConcurrentComponent />);

  // Trigger a low-priority state update (e.g., inside startTransition)
  const triggerButton = screen.getByRole('button');
  triggerButton.click();

  // Verify that reconciliation has been scheduled but not flushed yet
  expect(Scheduler).toHaveYielded([]);

  // Flush all pending reconciliation tasks
  Scheduler.unstable_flushAll();

  // Now the reconciliation is complete, and the DOM is updated
  expect(screen.getByText('New Concurrent Content')).toBeInTheDocument();
});

Method 3: Mocking a Custom Reconciler

If your application uses a custom renderer (such as react-three-fiber or a custom canvas renderer), you mock reconciliation by mocking the host config passed to react-reconciler.

Instead of importing the real reconciler, mock the dependency to return a simplified version of the node attachment operations.

jest.mock('react-reconciler', () => {
  return () => ({
    createContainer: jest.fn(),
    updateContainer: jest.fn(),
    getPublicRootInstance: jest.fn(),
    // Mock other required host config methods as empty functions
    appendChild: jest.fn(),
    removeChild: jest.fn(),
    insertInContainerBefore: jest.fn(),
  });
});

By mocking these host methods, you bypass the actual DOM operations while allowing React’s component lifecycles to execute normally during your tests.