How to Mock React Hydration
Mocking React hydration is a crucial technique for developers testing server-side rendered (SSR) applications. This article explains how to mock the hydration process and simulate hydration states in your unit tests. By controlling the hydration phase, you can prevent hydration mismatch warnings in your test suites and verify that your components render correctly on both the server and the client.
Understanding the Need to Mock Hydration
Hydration is the process where React attaches event listeners to the static HTML sent from the server, making the page interactive. In a testing environment like Jest or Vitest with React Testing Library, simulating this transition is difficult because the test runner typically operates in a pure client-side DOM environment (jsdom).
Mocking hydration helps you: 1. Prevent Mismatch Warnings: Avoid the “Text content did not match” errors in your test logs. 2. Test Dual-State Components: Verify components that render different content on the server versus the client (e.g., hiding a client-only widget until hydration is complete).
Mocking React 18
hydrateRoot
If you are testing the entry point of your application, you may need
to mock the actual hydrateRoot call from
react-dom/client. You can achieve this by mocking the
module in Jest.
import { hydrateRoot } from 'react-dom/client';
jest.mock('react-dom/client', () => {
return {
__esModule: true,
hydrateRoot: jest.fn(),
};
});
describe('Application Bootstrapping', () => {
it('should call hydrateRoot during initialization', () => {
// Trigger your app entry point here
require('./index.js');
expect(hydrateRoot).toHaveBeenCalled();
});
});Mocking the Hydration State in Components
Most hydration testing issues arise when components behave
differently before and after hydration. Developers often use a two-pass
rendering pattern with a state variable (e.g., isMounted or
isHydrated) to handle this.
Here is a typical component utilizing this pattern:
import { useState, useEffect } from 'react';
export function ClientOnlyComponent() {
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
setIsHydrated(true);
}, []);
if (!isHydrated) {
return <div>Loading server content...</div>;
}
return <div>Interactive Client Content</div>;
}To test both states of this component, you must control the React lifecycle in your tests.
Testing the Pre-Hydration (Server) State
To test the initial server render before useEffect
triggers the client-side hydration, you can mock or suppress the
lifecycle temporarily, or render the component using standard string
rendering:
import React from 'react';
import ReactDOMServer from 'react-dom/server';
test('renders server-side HTML before hydration', () => {
const html = ReactDOMServer.renderToString(<ClientOnlyComponent />);
expect(html).toContain('Loading server content...');
expect(html).not.toContain('Interactive Client Content');
});Testing the Post-Hydration (Client) State
Standard rendering in React Testing Library automatically triggers
useEffect hooks, which simulates the post-hydration
state.
import { render, screen } from '@testing-library/react';
import { ClientOnlyComponent } from './ClientOnlyComponent';
test('renders interactive content after hydration', () => {
render(<ClientOnlyComponent />);
// useEffect has run, so the client content should be visible
expect(screen.getByText('Interactive Client Content')).toBeInTheDocument();
expect(screen.queryByText('Loading server content...')).not.toBeInTheDocument();
});Silencing Hydration Mismatch Warnings in Tests
If your tests generate unavoidable hydration console errors due to slight differences in the test environment configuration, you can mock the global console object to ignore them.
Add this configuration to your Jest setup file
(jest.setup.js):
const originalError = console.error;
beforeAll(() => {
console.error = (...args) => {
if (/Warning: Did not expect server HTML to contain/.test(args[0])) {
return;
}
if (/Warning: Text content did not match/.test(args[0])) {
return;
}
originalError.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
});