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']);
});