Skip to the content.

Hydration is the handoff from server HTML to a live app

Hydration is the handoff from server HTML to a live app

Server-side rendering gives the user HTML they can see immediately, but that HTML is inert — the buttons do nothing, because the event handlers live in JavaScript that has not run yet. Hydration is the handoff that fixes that: the client bundle downloads, re-runs your components, walks the server-rendered DOM, and attaches the event listeners and state to the markup that is already there. It is the bridge between “looks ready” and “is ready,” and it is also where a specific, frustrating class of SSR bugs and costs live — because the client’s render has to agree with the server’s, exactly.

Server HTML is visible but inert until client JS hydrates it into a live app Server-rendered HTML is shown as visible but with dead buttons. The client bundle arrives and attaches handlers and state, turning the same DOM interactive. A gap between visible and interactive is marked. server HTMLvisible, inert hydrateattach handlers live appinteractive the gaplooks ready,clicks are dropped
Hydration adopts the existing DOM rather than rebuilding it. Between paint and hydration the page looks live but is not — the gap where clicks vanish.

Hydrate adopts the DOM; it does not rebuild it

The API difference is the whole idea. A pure client app creates the DOM; an SSR app hydrates the DOM the server already sent. Use the wrong one and React throws away the server HTML and re-renders from scratch, discarding the SSR benefit:

// SSR client entry: attach to existing markup — do NOT recreate it
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App />);   // adopts server DOM

// this would discard the server HTML and rebuild — wrong for SSR
// createRoot(root).render(<App />);

The mismatch bug: server and client must agree

Hydration assumes the client’s first render produces the same markup the server produced. If it does not — because you rendered the current time, a random value, or something that reads window — React sees a mismatch, warns, and may discard the server tree. The fix is to make the first client render deterministic and defer the browser-only value to after hydration:

function Clock() {
  const [now, setNow] = useState(null);          // same on server and first client render
  useEffect(() => { setNow(new Date()); }, []);  // browser-only value AFTER hydration
  return <span>{now ? now.toLocaleTimeString() : ""}</span>;  // no mismatch
}

The rule of thumb: anything that differs between server and browser (time, random, localStorage, viewport size) belongs in an effect, not in the render path.

Hydration is not free, hence the newer strategies

Even when it works, hydration costs: the client re-runs the whole component tree to attach handlers, so a large page pays a CPU bill right when the user wants to interact — the visible-but-not-interactive gap. That cost is exactly what the newer rendering strategies attack: partial / progressive hydration hydrates only the interactive islands and leaves static content alone; streaming SSR sends and hydrates the page in chunks so the top is live while the bottom is still arriving; server components push work off the client entirely. All of them are answers to the same question — how do we keep SSR’s fast first paint without paying to hydrate everything at once. Understanding plain hydration first is what makes those optimisations legible rather than magic. The render-strategy-choice exercise is where the trade between first paint and time-to-interactive becomes a concrete decision.