Hydration Basics: Understanding Client-Side Interactivity with React

A beginner-friendly guide to hydration in React, explaining how server-rendered HTML becomes an interactive web application on the client side.

React Web Development Frontend

Part 1: Beginner – The Basics

Hydration is a fancy way of saying, “Let’s take some HTML that was already rendered on the server and make it interactive with React on the client.” In simple terms, you send down HTML from your server so the user sees something right away. Later, React runs in the browser and “hydrates” that HTML by attaching event handlers and making it a live React app. This matters for a couple of big reasons: you get faster initial page loads (because the browser doesn’t have to build the entire DOM from JavaScript) and it’s also great for SEO and for users on slow connections or devices.

Imagine your server gave you this HTML:

<div id="root">
  <h1>Hello from Server!</h1>
  <button>Click me</button>
</div>

The browser shows this to the user immediately. Then your client-side JavaScript runs something like:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.hydrate(<App />, document.getElementById('root'));

Here, <App> is the same React component that produced the server HTML. ReactDOM.hydrate looks at the existing HTML inside #root and attaches its React app on top of it. It doesn’t throw away the HTML; instead, it “reuses” it and just wires up the event handlers. For example, if App had a button with an onClick, that click handler becomes active now.

Why hydration? Well, without it, the user would see a blank page while React’s JavaScript downloads and renders everything. With hydration, the user sees the content right away, then gets a working app as soon as the JS finishes loading. This is especially helpful for SEO and for users on slow connections or devices. In React 18+, we often use:

import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

This is the newer way to hydrate in React 18, setting up a concurrent root. But underneath, the goal is the same: start with server-rendered HTML and make it into an interactive React app on the client.

Quick Code Example

Server-side (Node.js) Using ReactDOMServer to render HTML:

import express from 'express';
import ReactDOMServer from 'react-dom/server';
import App from './App';

const app = express();

app.get('/', (req, res) => {
  const appHtml = ReactDOMServer.renderToString(<App />);
  res.send(\`
    <!DOCTYPE html>
    <html><body>
      <div id="root">\${appHtml}</div>
      <script src="/client.js"></script>
    </body></html>
  `);
});
app.listen(3000);

Client-side (Browser) Hydrating the app:

import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.hydrate(<App />, document.getElementById('root'));

The diagram below illustrates the hydration process:

  • The server renders the React app to HTML and sends it to the browser.
  • The browser displays the server-rendered HTML immediately.
  • The client-side JavaScript runs and React hydrates the HTML, attaching event handlers and making the app interactive.
  • The user can now interact with the app seamlessly.
Hydration Diagram

In this setup, when the browser loads the page, it sees the HTML from the server (the <div id="root">...</div> content). Then it runs client.js, which calls ReactDOM.hydrate. That makes the page interactive by telling React: “Hey, attach to this server-rendered DOM.”

Keep in mind:

  1. The HTML rendered by the server and the HTML that React expects on the client should match.
  2. If they don’t match (for example, because of different data or timing),
  3. React might warn you and replace parts of the DOM. But in a typical setup,
  • Hydration is smooth and is what lets us combine the best of server rendering (fast first paint) and client rendering (dynamic interactivity).