6 min read
0%

Hydration in SSR

Back to Blog
Hydration in SSR

Hydration in SSR

Hydration is the process of attaching React’s event system and state to server-rendered HTML. The server sends static markup; the client makes it interactive.

The Process

  1. Server renders the component tree to an HTML string
  2. Browser receives and displays the HTML immediately (fast first paint)
  3. React downloads and parses the JS bundle
  4. React renders the component tree in memory
  5. React walks the existing DOM and attaches event listeners, reconciling where needed
  6. App is now interactive

Steps 4–6 are hydration. The user sees content before step 3 completes but can’t interact until step 6.

Server-Side Rendering

// server.js (Node)
import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);
// Sends: <div id="root"><div class="header">...</div></div>

Client-Side Hydration

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

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

hydrateRoot instead of createRoot — it expects existing DOM and attaches to it rather than creating fresh nodes.

Hydration Mismatches

If the client renders different output than the server, React warns and patches the DOM:

Warning: Expected server HTML to contain matching client render...

Common causes:

  • Reading browser-only APIs (localStorage, window) during render
  • Date/time values that differ between server and client
  • Random IDs generated during render
  • Content that depends on auth state not available server-side

Fixing Mismatches

Suppress for intentional differences:

<time suppressHydrationWarning dateTime={serverDate}>
  {clientFormattedDate}
</time>

Defer browser-only content:

function ClientOnly({ children }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return children;
}

Move to effects: access localStorage, window inside useEffect, not during render.

Streaming SSR (React 18)

React 18 introduced renderToPipeableStream for streaming HTML. The server sends the shell immediately and streams in content as it resolves:

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    response.setHeader('content-type', 'text/html');
    pipe(response);
  },
});

Suspended components render their fallback in the shell, then stream in the real content when ready. Hydration is also incremental — interactive parts become interactive as their chunks arrive.


Canvas is not supported in your browser