How to Mock Event Pooling in React

React’s legacy event pooling system, which reused synthetic events to improve performance in React 16 and earlier, often requires special handling during unit testing. This article explains how event pooling works, why it causes issues in tests, and how to effectively mock synthetic events and the event.persist() method using Jest and React Testing Library.

Understanding Event Pooling in React

In React 16 and earlier, React reused the same SyntheticEvent object across different events to save memory. Once an event handler executed, React nullified all properties on the event object.

To use an event asynchronously (such as inside a setTimeout or an API promise), developers had to call e.persist() to remove the event from the pool. While React 17 completely removed event pooling, you may still encounter legacy codebases or third-party libraries where you must mock this behavior.

Why Mock Event Pooling?

When testing event handlers that call e.persist(), passing a simple object mock like { target: { value: 'test' } } will result in a runtime error:

TypeError: e.persist is not a function

To prevent this error and ensure your event handlers run correctly under test conditions, you must mock the persist function on your event objects.

How to Mock event.persist() in Jest

The most common way to mock React’s event pooling in unit tests is by providing a mock implementation of the persist method using Jest.

Basic Mocking in Component Tests

When simulating an event using React Testing Library or Enzyme, you can pass a custom event object that includes a mocked persist function.

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

test('should handle asynchronous input change', () => {
  render(<MyComponent />);
  
  const input = screen.getByRole('textbox');

  // Create a mock event with a mocked persist function
  const mockEvent = {
    target: { value: 'Hello World' },
    persist: jest.fn() // Mocks e.persist()
  };

  // Fire the event with the mocked event object
  fireEvent.change(input, mockEvent);

  expect(mockEvent.persist).toHaveBeenCalled();
  expect(screen.getByText('Hello World')).toBeInTheDocument();
});

Mocking Globally or in Helper Functions

If you have many tests that require mock events with pooled behavior, you can create a helper utility to generate these mock events.

export const createMockHtmlEvent = (values = {}) => {
  return {
    ...values,
    persist: jest.fn(),
    preventDefault: jest.fn(),
    stopPropagation: jest.fn(),
  };
};

// Usage in a test
const event = createMockHtmlEvent({ target: { value: 'test-value' } });

Simulating Actual Pooling Behavior (Advanced)

If you specifically need to test that an event handler fails without e.persist() (to ensure regression protection for legacy React 16 applications), you can mock the pooling mechanism by nullifying the properties after the event loop tick.

function createPooledEvent(data) {
  const event = {
    ...data,
    isPersisted: false,
    persist() {
      this.isPersisted = true;
    }
  };

  // Simulate React nullifying the properties asynchronously
  setTimeout(() => {
    if (!event.isPersisted) {
      Object.keys(data).forEach((key) => {
        event[key] = null;
      });
    }
  }, 0);

  return event;
}

By utilizing these mocking strategies, you can ensure your test suites remain robust and error-free when handling legacy React codebases that rely on event pooling.