Skip to content

Hydration — Attaching Event Handlers to Server-Rendered HTML

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Hydration. We cover key concepts, practical examples, and best practices to help you master this topic.

Hydration is the Process where React attaches event handlers to server-rendered HTML, making static server content interactive without re-rendering or replacing the existing DOM nodes.

What You'll Learn

By the end of this tutorial, you will understand what hydration is, how React hydration works under the hood, the difference between render and hydrate, how to avoid hydration mismatch errors, and best practices for efficient hydration.

Why It Matters

Hydration is the bridge between SSR and interactivity. Without hydration, server-rendered pages are static — users cannot click buttons, submit forms, or interact. Proper hydration is essential for a good user experience. Poor hydration causes delays in interactivity, flickering, and console errors that confuse developers.

Real-World Use

A large e-commerce site noticed a 3-second delay between page display and button interactivity. Analysis showed they were hydrating the entire application at once, blocking the main thread. After implementing selective hydration — hydrating only visible, interactive components first — the time-to-interactive dropped to 0.5 seconds.

Hydration Process
    ┌──────────────────────────────────────────────────────────┐
    │              React Hydration Process                     │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  1. Server sends complete HTML                           │
    │     <button>Click me</button>                            │
    │     (visible but not interactive)                        │
    │                                                          │
    │  2. Browser displays HTML immediately                    │
    │     User sees the button                                │
    │                                                          │
    │  3. JavaScript bundle loads                              │
    │     React library is downloaded and parsed               │
    │                                                          │
    │  4. hydrateRoot() is called                              │
    │     React traverses existing DOM                         │
    │                                                          │
    │  5. React reconciles — matches virtual DOM to real DOM  │
    │     If they match: attaches event handlers               │
    │     If they differ: (mismatch error or replaces DOM)     │
    │                                                          │
    │  6. Button is now interactive                            │
    │     onClick handler attached                             │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of hydration like painting a metal statue. The server creates the statue (HTML) and places it in the park (browser). Hydration is the painting process — it does not change the shape of the statue, it just adds color and detail (event handlers, state). If the statue's shape changed between creation and painting (mismatch), the paint would not fit correctly.

Hydrate vs Render

// CSR — createRoot + render
// Creates DOM nodes from scratch
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);
// React creates all DOM nodes, no existing HTML to match

// SSR — hydrateRoot
// Attaches to existing DOM nodes
import { hydrateRoot } from 'react-dom/client';

hydrateRoot(
    document.getElementById('root'),
    <App />
);
// React expects existing DOM that matches server-rendered output
// Does NOT create new DOM — attaches event handlers to existing nodes

// Key difference:
// createRoot: "I own this DOM, I created it"
// hydrateRoot: "Someone else created this DOM, let me make it interactive"

Hydration Mismatch Example

// Server component (rendered via renderToString)
function Timestamp() {
    // This runs on the server
    const serverTime = new Date().toISOString();
    return <p>Server time: {serverTime}</p>;
}

// Same component on client (hydrated via hydrateRoot)
function Timestamp() {
    // This runs on the client — DIFFERENT TIME
    const clientTime = new Date().toISOString();
    return <p>Server time: {clientTime}</p>;
}

// The HTML rendered on the server:
// <p>Server time: 2026-06-28T10:00:00.000Z</p>
//
// But during hydration, React expects:
// <p>Server time: 2026-06-28T10:00:00.000Z</p>
//
// Instead it finds:
// <p>Server time: 2026-06-28T10:00:05.000Z</p>
// (different time — 5 seconds later)
//
// React throws: Hydration failed because the initial UI does not match
// What was rendered on the server.

// Fix: Use useEffect for client-only code
function Timestamp() {
    const [time, setTime] = React.useState(null);

    React.useEffect(() => {
        setTime(new Date().toISOString());
    }, []);

    return <p>Server time: {time || 'Loading...'}</p>;
}

// Server renders: <p>Server time: Loading...</p>
// Client hydrates: matches "Loading..."
// useEffect runs: updates to actual time

Efficient Hydration Strategies

// 1. Defer hydration for below-fold content
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
    return (
        <div>
            <h1>Dashboard</h1>
            <p>This content is hydrated immediately.</p>

            <Suspense fallback={<div>Loading chart...</div>}>
                <HeavyChart />
            </Suspense>
        </div>
    );
}

// 2. Progressive hydration with custom hook
function useHydration() {
    const [isHydrated, setIsHydrated] = React.useState(false);

    React.useEffect(() => {
        // Defer hydration to next frame using requestIdleCallback
        const id = requestIdleCallback(() => {
            setIsHydrated(true);
        });
        return () => cancelIdleCallback(id);
    }, []);

    return isHydrated;
}

function BelowFoldWidget() {
    const isHydrated = useHydration();

    if (!isHydrated) {
        // Show static placeholder during initial hydration
        return <div className="widget-placeholder" />;
    }

    return <InteractiveWidget />;
}

// 3. Selective hydration with isHydrated flag
function Page() {
    const [interactive, setInteractive] = React.useState(false);

    React.useEffect(() => {
        // Mark as interactive after main content hydrates
        const timer = setTimeout(() => setInteractive(true), 100);
        return () => clearTimeout(timer);
    }, []);

    return (
        <div>
            <MainContent /> {/* Hydrated immediately */}
            {interactive && <NonCriticalFeature />} {/* Delayed */}
        </div>
    );
}

Common Mistakes

  1. Hydration mismatch from date/time/random values. Code that produces different output on server vs client (Date.now(), Math.random(), crypto.randomUUID) causes hydration errors. Use useEffect for such values.
  2. Missing data during hydration. If the server renders with data but the client does not have the same data during hydration, the HTML will not match. Always pass server data to the client via JSON script tags.
  3. Hydrating the entire app at once. Hydrating everything blocks the main thread. Use progressive hydration to hydrate visible, interactive components first.
  4. Using createRoot instead of hydrateRoot. createRoot replaces existing DOM with new DOM, causing flicker and losing the SSR benefit. Always use hydrateRoot for SSR applications.
  5. Not handling hydration errors gracefully. Hydration errors in development show warnings. In production, React falls back to client-side rendering for the mismatched subtree but the error may cause UX issues.

Practice Questions

  1. What is the purpose of hydration in SSR applications?
  2. How does hydrateRoot differ from createRoot?
  3. What causes hydration mismatch errors and how do you fix them?
  4. What is progressive hydration and why is it important?
  5. How do you pass server data to the client for hydration?

Challenge: Build an SSR page with intentional hydration mismatches: a timestamp component that differs between server and client, a random number generator, and a component that uses localStorage. Fix each mismatch using appropriate patterns (useEffect, state initialization). Then implement progressive hydration for a heavy chart component below the fold.

FAQ

What happens if hydration fails silently?

React logs a warning in development. In production, React attempts to recover by re-rendering on the client, which may cause a flash of changed content.

Does hydration re-render all components?

No. Hydration traverses the existing DOM and attaches event handlers. It does not re-render unless there is a mismatch. This makes hydration fast.

Can I hydrate only part of a page?

Yes. You can use multiple hydrateRoot calls for different sections of the page, or use progressive hydration patterns to control when components become interactive.

How long does hydration take?

Hydration time depends on the number of components and the complexity of the tree. A typical page hydrates in 50-500ms. Large apps may take 1-3 seconds without optimization.

Does hydration work with TypeScript?

Yes. TypeScript works with hydration. The server and client code are the same TypeScript files compiled for their respective environments.

Mini Project

Build an SSR application that demonstrates hydration concepts: a page with server-rendered content that hydrates on the client, a timestamp component that shows how to avoid hydration mismatches with useEffect, a lazy-loaded chart component below the fold that hydrates progressively, and measurement of time-to-interactive before and after progressive hydration.

What's Next

You understand hydration. Now learn about Hydration Mismatch to debug and fix common SSR hydration issues.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro