How to Test React Strict Mode

React Strict Mode is an essential development tool that helps identify potential issues in an application by intentionally double-rendering components and double-invoking effects. To ensure your components are resilient to these behaviors, you must write tests that execute within this strict environment. This article provides a straightforward guide on how to configure your testing environment using Jest and React Testing Library to verify that your components function correctly under React Strict Mode.

Enabling Strict Mode in Tests

To test how a component behaves in Strict Mode, you must wrap the component under test inside the <React.StrictMode> component. React Testing Library renders components exactly as they would run in the browser, meaning this wrapper will trigger Strict Mode’s double-rendering and double-effect-firing behaviors during your tests.

Here is a basic setup using React Testing Library and Jest:

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

test('renders correctly under Strict Mode', () => {
  render(
    <React.StrictMode>
      <MyComponent />
    </React.StrictMode>
  );

  // Assert that the component behaves as expected
  expect(screen.getByRole('heading')).toHaveTextContent('Hello World');
});

Testing for Double-Mounting and Cleanup Issues

In React 18 and later, Strict Mode mounts, unmounts, and mounts your components again on initial render. This is designed to expose missing cleanup functions in useEffect hooks. If your component fails to clean up subscriptions, event listeners, or timers, this double-mount behavior will trigger bugs during your tests.

Example: Testing Event Listeners

If a component registers a global event listener but forgets to clean it up, the double-mount in Strict Mode will register the listener twice. You can test and catch this behavior by spying on the global window object.

import React, { useEffect } from 'react';
import { render } from '@testing-library/react';

// Component under test
function EventComponent() {
  useEffect(() => {
    const handleResize = () => {};
    window.addEventListener('resize', handleResize);
    
    // Cleanup function (if omitted, the test below will catch the duplicate listener)
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>Event Component</div>;
}

test('does not duplicate event listeners in Strict Mode', () => {
  const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
  const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');

  render(
    <React.StrictMode>
      <EventComponent />
    </React.StrictMode>
  );

  // Strict Mode mounts (1), unmounts (2), and mounts again (3)
  // Total additions should match total removals plus the active one
  expect(addEventListenerSpy).toHaveBeenCalledTimes(2);
  expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);

  addEventListenerSpy.mockRestore();
  removeEventListenerSpy.mockRestore();
});

Catching Deprecation and Strict Mode Warnings

React Strict Mode warns you about unsafe lifecycles, legacy API usage (like findDOMNode), and deprecated context APIs by printing warnings to console.error. To ensure your codebase remains compliant, you can write a test that fails if any console errors or warnings are detected during rendering.

describe('Strict Mode Compliance', () => {
  let consoleErrorSpy;

  beforeAll(() => {
    consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation((message) => {
      // Fail the test if a Strict Mode warning is logged
      if (message.includes('Warning:')) {
        throw new Error(`React Warning detected in Strict Mode: ${message}`);
      }
    });
  });

  afterAll(() => {
    consoleErrorSpy.mockRestore();
  });

  test('should not produce any React console warnings', () => {
    render(
      <React.StrictMode>
        <MyComponent />
      </React.StrictMode>
    );
  });
});

By wrapping your components in <React.StrictMode> during testing and monitoring both component lifecycles and console outputs, you can prevent memory leaks and deprecation issues from reaching production.