hydrateRoot

Attaches React to server-rendered HTML already inside a browser DOM node.

Signature
const root = hydrateRoot(domNode, reactNode, options?)
  • Added in React 18.0.0

Parameters

ParameterTypeDescription
$domNodeElementA DOM element that was rendered as the root element on the server.
$reactNodeReact nodeThe React node used to render the existing HTML — usually JSX such as <App />, but also an element from createElement().
$optionsoptionalobjectOptional configuration for the root.
$options.onCaughtErroroptional(error, errorInfo) => voidCalled when React catches an error inside an Error Boundary. Receives the error and an errorInfo object containing componentStack.
$options.onUncaughtErroroptional(error, errorInfo) => voidCalled when an error is thrown and not caught by any Error Boundary. Receives the error and an errorInfo object containing componentStack.
$options.onRecoverableErroroptional(error, errorInfo) => voidCalled when React automatically recovers from an error, including hydration mismatches. Some errors expose the original cause as error.cause.
$options.identifierPrefixoptionalstringA prefix React uses for IDs generated by useId. Must match the prefix passed to the server renderer.

Return value

RootA root object with two methods: render and unmount.

Errors

Throws if domNode is not a DOM element; a hydration mismatch does not throw but is reported through onRecoverableError while React re-renders that subtree on the client.

Changelog

  • PHP 18.0.0Replaced ReactDOM.hydrate and added selective hydration, so a tree wrapped in Suspense can hydrate in pieces and prioritise the part the user interacts with first.
  • PHP 19.0.0Added the onUncaughtError and onCaughtError options, and hydration mismatches now log a single error containing a diff of the server and client output instead of several separate warnings.

hydrateRoot is the client half of server rendering. The HTML is already on screen; React walks it, attaches its event handling and component state to the existing nodes, and from then on updates them like any other tree. Nothing is re-created, so the page does not flash.

When to reach for it

  • The client entry of an app whose HTML comes from renderToPipeableStream, renderToReadableStream, prerender, or a static build. If a framework owns your entry file, it already calls this.
  • Making pre-rendered marketing or docs pages interactive without paying to build the DOM twice.

What trips people up

The first client render must produce exactly the markup the server produced. Date.now(), Math.random(), window checks, locale-dependent formatting and browser-only extensions all diverge, and React responds by discarding that subtree's HTML and re-rendering it — correct, but slow and visible. Move such values into an effect, or mark a single node with suppressHydrationWarning.

Note also that the tree is passed as the second argument, not through render. The returned root does have render, but calling it before hydration finishes throws the server HTML away and falls back to client rendering.

Example

Example · jsx
// ---------- server.js ----------
import { renderToPipeableStream } from "react-dom/server";
import App from "./App.jsx";

function handleRequest(request, response) {
  const { pipe } = renderToPipeableStream(<App />, {
    identifierPrefix: "app-",
    bootstrapScripts: ["/client.js"],
    onShellReady() {
      response.setHeader("content-type", "text/html");
      pipe(response);
    },
  });
}

// ---------- client.js ----------
import { hydrateRoot } from "react-dom/client";
import App from "./App.jsx";

const root = hydrateRoot(document.getElementById("root"), <App />, {
  identifierPrefix: "app-", // must match the server
  onRecoverableError(error, errorInfo) {
    console.error("hydration recovered", error.message, errorInfo.componentStack);
  },
});

// ---------- App.jsx (shared by both) ----------
import { useState } from "react";

export default function App() {
  const [likes, setLikes] = useState(0);
  return (
    <main>
      <h1>Server-rendered page</h1>
      <button onClick={() => setLikes(likes + 1)}>Likes: {likes}</button>
    </main>
  );
}

When to use it

  • Wiring the client entry of a server-rendered app to the HTML the server streamed
  • Making a statically pre-rendered page interactive without rebuilding its DOM
  • Reporting hydration mismatches to your error tracker through onRecoverableError
  • Keeping useId values stable across the server/client boundary with a shared identifierPrefix

Frequently asked questions

What causes a hydration mismatch error in React?

The first client render produced different markup from the server's — commonly from Date.now(), Math.random(), locale formatting, reading window or localStorage during render, invalid HTML nesting, or a browser extension editing the DOM. React logs the error and re-renders the affected subtree on the client.

When should I use hydrateRoot instead of createRoot?

Use hydrateRoot when the container already holds HTML that React generated on the server or at build time. If the container is empty, use createRoot — hydrateRoot against empty markup reports a mismatch for the whole tree.

Why does hydrateRoot take the component as an argument?

Because React needs the tree up front to match it against the existing HTML as it walks the DOM. That is why there is no separate render call for the initial pass; the returned root's render method is for later updates.

More examples

Values that only exist in the browser

LastVisited renders the same "loading…" text on both sides and only reaches for localStorage in an effect, which runs after hydration. BuildStamp takes its date as a prop instead of calling new Date(), so the string in the HTML is byte-identical to the one the client computes. This two-pass shape is the standard fix for a "text content did not match" error.

Example · jsx
import { useEffect, useState } from "react";

function LastVisited() {
  // Never read localStorage or Date during the first render:
  // the server has neither, so the HTML would not match.
  const [visited, setVisited] = useState(null);

  useEffect(() => {
    setVisited(localStorage.getItem("lastVisited"));
  }, []);

  return <p>Last visited: {visited ?? "loading…"}</p>;
}

function BuildStamp({ isoDate }) {
  // Formatted on the server and passed down, so both sides agree.
  return <p>Built: {isoDate}</p>;
}

export default function Footer() {
  return (
    <footer>
      <BuildStamp isoDate="2024-11-02T00:00:00.000Z" />
      <LastVisited />
    </footer>
  );
}

Allowing one node to differ

suppressHydrationWarning is a one-level escape hatch: React silences the mismatch for this element's own text and attributes, but the exemption does not extend to its children. It also changes what you see — React will not attempt to patch mismatched text content, so the node keeps the server's string (the UTC-formatted date here) until some later re-render replaces it, instead of paying for the client re-render React normally uses to recover from a mismatch. Use it only for genuinely unavoidable differences such as locale-formatted timestamps — for anything else, fix the divergence.

Example · jsx
function ServerTime({ isoDate }) {
  // The server prints UTC; the browser prints the visitor's locale.
  // suppressHydrationWarning silences the mismatch for this node only.
  return (
    <time dateTime={isoDate} suppressHydrationWarning>
      {new Date(isoDate).toLocaleString()}
    </time>
  );
}

export default function Article() {
  return (
    <article>
      <h2>Release notes</h2>
      <p>
        Published <ServerTime isoDate="2024-11-02T00:00:00.000Z" />
      </p>
    </article>
  );
}

Updating and tearing down a hydrated root

hydrateRoot returns the same kind of root object as createRoot, so render and unmount work identically once hydration has completed. The one restriction is timing: calling root.render before hydration finishes discards the server HTML and re-renders the tree from scratch on the client, which defeats the point of server rendering.

Example · jsx
import { hydrateRoot } from "react-dom/client";
import App from "./App.jsx";

const container = document.getElementById("root");
const root = hydrateRoot(container, <App page="home" />);

// After hydration, the root behaves like any client root.
function navigate(page) {
  root.render(<App page={page} />);
}

// Handing the container back to the host page.
function teardown() {
  root.unmount();
}

Discussion

  • Be the first to comment on this lesson.