How to Test React Hydration

React hydration bridges the gap between server-rendered HTML and client-side interactivity, but mismatches between the server and client markup can cause UI bugs and performance regressions. This article explains how to effectively test React hydration and catch mismatch errors using browser developer tools, automated end-to-end testing frameworks, and unit testing configurations.

1. Manual Testing with Browser Developer Tools

The fastest way to test for hydration issues during development is to inspect the browser console. When React detects a difference between the server-rendered HTML and the client-rendered virtual DOM, it logs a warning in development mode.

To test this manually: 1. Run your React application in development mode. 2. Open your browser’s Developer Tools (F12) and navigate to the Console tab. 3. Look for warnings containing phrases like: * Warning: Did not expect server HTML to contain a... * Warning: Text content did not match... * An error occurred during hydration...

If these warnings appear, it means your server-rendered HTML does not match what the client expected on the initial render. Common culprits include using browser-only globals (like window or localStorage) during the initial render or using dynamic data like new Date() or Math.random().

2. Automated E2E Testing with Playwright

Manual testing is prone to human error, making automated testing essential. Because hydration is a browser-only process, end-to-end (E2E) testing tools like Playwright are ideal for catching hydration failures in CI/CD pipelines.

You can configure Playwright to listen for console warnings and fail the test if a hydration warning is detected.

import { test, expect } from '@playwright/test';

test('should load the page without hydration errors', async ({ page }) => {
  const warnings = [];

  // Listen for console warnings and errors
  page.on('console', (msg) => {
    if (msg.type() === 'warning' || msg.type() === 'error') {
      if (msg.text().includes('hydration') || msg.text().includes('did not match')) {
        warnings.push(msg.text());
      }
    }
  });

  await page.goto('http://localhost:3000');

  // Verify that no hydration warnings were captured
  expect(warnings).toEqual([]);
});

3. Unit Testing with React Testing Library

You can test how your components behave during hydration at the unit level using React Testing Library and react-dom/client. Instead of using standard rendering, you can mock the SSR output and call React’s hydrateRoot to test the hydration process directly.

import { screen } from '@testing-library/react';
import { hydrateRoot } from 'react-dom/client';
import React from 'react';

test('hydrates correctly without errors', () => {
  const container = document.createElement('div');
  
  // 1. Simulate Server-Side Rendered HTML
  container.innerHTML = '<button id="btn">Click me</button>';
  document.body.appendChild(container);

  const consoleWarnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

  // 2. Hydrate the container with the client component
  const App = () => <button id="btn">Click me</button>;
  hydrateRoot(container, <App />);

  // 3. Assert no hydration warnings were triggered
  expect(consoleWarnSpy).not.toHaveBeenCalled();

  consoleWarnSpy.mockRestore();
  document.body.removeChild(container);
});

4. Monitoring Hydration Failures in Production

Sometimes hydration errors only occur in production under specific conditions. You can capture these errors programmatically by leveraging the onRecoverableError callback inside the hydrateRoot configuration. This allows you to log hydration errors directly to your error-tracking service (such as Sentry or LogRocket).

import { hydrateRoot } from 'react-dom/client';
import App from './App';

hydrateRoot(
  document.getElementById('root'),
  <App />,
  {
    onRecoverableError: (error, errorInfo) => {
      console.error('Hydration error captured:', error);
      // Send error telemetry here
    }
  }
);