How to Test useInsertionEffect in React

Testing useInsertionEffect in React requires understanding its unique timing, as it fires synchronously before any DOM mutations and layout effects. This article provides a straightforward guide on how to test this hook using React Testing Library and Jest, ensuring your dynamic CSS-in-JS injections or global style manipulations are verified correctly.

The Challenge of Testing useInsertionEffect

useInsertionEffect is designed specifically for CSS-in-JS library authors to inject styles into the document before read operations in useLayoutEffect occur. Because it runs synchronously before the DOM is mutated, standard component rendering tests can sometimes miss timing-specific bugs.

To test it effectively, you must assert that: 1. The side effect (such as injecting a <style> tag) actually occurs. 2. The timing of the effect runs before other layout effects. 3. Cleanup functions run correctly when the component unmounts.

Step-by-Step Testing Guide

Here is a practical example of how to test a component utilizing useInsertionEffect.

1. Create the Component

Below is a component that uses useInsertionEffect to inject a dynamic theme style tag into the document head.

import React, { useInsertionEffect } from 'react';

export function ThemeProvider({ themeColor }) {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.dataset.testid = 'dynamic-theme';
    style.textContent = `.custom-text { color: ${themeColor}; }`;
    document.head.appendChild(style);

    return () => {
      document.head.removeChild(style);
    };
  }, [themeColor]);

  return <span className="custom-text">Styled TextSpan</span>;
}

2. Write the Unit Test

Using Jest and React Testing Library, you can render the component and inspect the document head to verify the insertion and deletion of the style tag.

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

describe('ThemeProvider with useInsertionEffect', () => {
  test('injects style tag into the head and cleans up on unmount', () => {
    // Render the component
    const { unmount } = render(<ThemeProvider themeColor="rgb(255, 0, 0)" />);

    // Assert that the style tag exists in the head
    const styleTag = document.querySelector('style[data-testid="dynamic-theme"]');
    expect(styleTag).toBeInTheDocument();
    expect(styleTag.textContent).toContain('color: rgb(255, 0, 0)');

    // Unmount the component to trigger cleanup
    unmount();

    // Assert that the style tag has been removed
    expect(styleTag).not.toBeInTheDocument();
  });
});

3. Testing Execution Order

If your test needs to verify that useInsertionEffect indeed fires before useLayoutEffect and useEffect, you can spy on mock functions triggered within each hook.

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

test('runs useInsertionEffect before layout and paint effects', () => {
  const executionOrder = [];

  function HookOrderComponent() {
    useInsertionEffect(() => {
      executionOrder.push('insertion');
    });

    useLayoutEffect(() => {
      executionOrder.push('layout');
    });

    useEffect(() => {
      executionOrder.push('effect');
    });

    return <div>Timing Test</div>;
  }

  render(<HookOrderComponent />);

  // Assert correct React lifecycle order
  expect(executionOrder).toEqual(['insertion', 'layout', 'effect']);
});