How to Implement React Hydration

React hydration is the process of mapping client-side JavaScript event listeners to static HTML pages rendered by a server. This guide provides a straightforward explanation of React hydration, why it is essential for Server-Side Rendering (SSR), and a step-by-step technical walkthrough on how to implement it using modern React 18+ APIs.

Understanding React Hydration

When using Server-Side Rendering (SSR), the server generates a static HTML representation of your React component tree and sends it to the browser. This allows the user to see the page content almost instantly. However, this static HTML is not yet interactive.

Hydration is the process where React preserves the existing HTML structure rendered by the server, boots up in the browser, and attaches event listeners (like clicks, form submissions, and keyboard inputs) to the DOM nodes.

Step-by-Step Implementation

To implement React hydration, you need a two-part setup: a server-side script to render the initial HTML and a client-side script to hydrate that HTML.

1. Server-Side Rendering (The Server)

First, you must render your React component to an HTML string on your Node.js server. In React 18, you can use renderToString or the streaming API renderToPipeableStream from the react-dom/server package.

// server.js (Express example)
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './src/App';

const app = express();

app.use(express.static('public')); // Serve client-side JS bundle

app.get('*', (req, res) => {
  const appHtml = renderToString(<App />);

  const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>React Hydration Example</title>
    </head>
    <body>
      <div id="root">${appHtml}</div>
      <script src="/bundle.js"></script> <!-- Client-side React bundle -->
    </body>
    </html>
  `;

  res.send(html);
});

app.listen(3000, () => console.log('Server running on port 3000'));

2. Client-Side Hydration (The Client)

Once the browser loads the static HTML and fetches the compiled JavaScript bundle (bundle.js), the client-side entry point takes over. Instead of using createRoot (which is used for purely client-rendered apps and replaces the DOM), you must use hydrateRoot from react-dom/client.

// client.js (Client entry point)
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';

const domNode = document.getElementById('root');

// Hydrate the server-rendered HTML
hydrateRoot(domNode, <App />);

Avoiding Hydration Mismatches

For hydration to work correctly, the HTML generated on the server must exactly match the initial HTML generated on the client. If React detects a difference (e.g., different text content, differing attributes, or timestamp mismatches), it will throw a “Hydration Mismatch” warning.

To prevent mismatches: * Avoid browser-only globals: Do not reference window, document, or localStorage during the initial rendering phase. Wrap these in a useEffect hook, which only executes on the client. * Synchronize dynamic data: Ensure date objects, random numbers, or localized strings generate the same values on both the server and client. * Use suppressHydrationWarning cautiously: If a mismatch is unavoidable (such as displaying a timestamp), you can add the suppressHydrationWarning={true} attribute to the specific element to silence the error. Note that this only works one level deep.