Hydration Proof

Search documentation

Find a page or section

Expected server HTML to contain a matching element

React 18's warning for an element on one side only.

Expected server HTML to contain a matching <div> in <div> is a React 18 development warning: the first browser render has an element that the server's HTML does not have. "Did not expect server HTML to contain" is the reverse. Something renders on one side only, or the browser moved invalid markup. Render the same elements on both sides.

The error

The two forms, as React 18 prints them:

Warning: Expected server HTML to contain a matching <div> in <div>.
Warning: Did not expect server HTML to contain a <div> in <div>.

The tags change with your markup. These are the variants people see most:

Warning: Expected server HTML to contain a matching <div> in <body>.
Warning: Expected server HTML to contain a matching <tr> in <table>.
Warning: Expected server HTML to contain a matching <table> in <div>.
Warning: Expected server HTML to contain a matching <meta> in <head>.
Warning: Expected server HTML to contain a matching text node for "Loading…" in <div>.
Warning: Did not expect server HTML to contain the text node "Loading…" in <div>.

Right after the warning, React 18 throws "Hydration failed because the initial UI does not match what was rendered on the server", or #418 in production. React 19 shows the same thing as a + line (only in the browser) or a - line (only on the server) in the diff of its hydration error.

What expected server HTML to contain a matching element means

Read the warning as "child in parent":

WarningThe first tag isMeaning
Expected server HTML to contain a matching <X> in <Y>In the browser render onlyThe server did not render <X> there, or the browser moved it
Did not expect server HTML to contain a <X> in <Y>In the server HTML onlyThe browser render has nothing there, or something else

The component stack printed under the warning names the component that renders the parent. React 18 logs only the first difference per page, then renders the page again in the browser.

Common causes

An element rendered on one side only

A typeof window check, a value from localStorage, a "mounted" flag read during render or a feature flag the browser reads again: the server renders nothing (or a placeholder) and the browser renders the element. See browser-only APIs and storage.

A table without <tbody>

The <tr> in <table> variant is almost always this. The HTML parser adds a <tbody> around rows it finds directly in a <table>, so React finds a <tbody> where it expected your <tr>. See validateDOMNesting.

Invalid nesting the browser repaired

A <div>, <table> or list inside a <p> is moved out of the <p> while the page is parsed. React then expects the element inside the paragraph and does not find it. See div cannot be a descendant of p.

Something else changed the page first

For <div> in <body>, a browser extension or a third-party script (chat widgets, consent banners) often inserted or removed elements before React loaded. For <meta> in <head>, check tags rendered from browser-only values and scripts that add their own tags. See browser extensions and third-party scripts.

How to fix it

  1. Match the tags to your code. The warning names the child and the parent. The component stack names the component.

  2. Wrap table rows in <tbody>. Before:

    components/prices.tsx
    type Row = { id: string; name: string; price: string };
     
    export function Prices({ rows }: { rows: Row[] }) {
      return (
        <table>
          {rows.map((row) => (
            <tr key={row.id}>
              <td>{row.name}</td>
              <td>{row.price}</td>
            </tr>
          ))}
        </table>
      );
    }

    After:

    components/prices.tsx
    type Row = { id: string; name: string; price: string };
     
    export function Prices({ rows }: { rows: Row[] }) {
      return (
        <table>
          <tbody>
            {rows.map((row) => (
              <tr key={row.id}>
                <td>{row.name}</td>
                <td>{row.price}</td>
              </tr>
            ))}
          </tbody>
        </table>
      );
    }
  3. Render browser-only elements after hydration. Before, the banner exists only in the browser render:

    components/cookie-banner.tsx
    "use client";
     
    export function CookieBanner() {
      if (typeof window === "undefined") return null;
      if (localStorage.getItem("consent")) return null;
      return <div className="banner">We use cookies.</div>;
    }

    After, both renders start with nothing, and an effect shows it:

    components/cookie-banner.tsx
    "use client";
     
    import { useEffect, useState } from "react";
     
    export function CookieBanner() {
      const [show, setShow] = useState(false);
     
      useEffect(() => {
        setShow(!localStorage.getItem("consent"));
      }, []);
     
      if (!show) return null;
      return <div className="banner">We use cookies.</div>;
    }
  4. Test in a clean browser profile when the parent is <body> or <head>. If the warning disappears, an extension caused it.

Find every instance

hydration-proof reports an element only the browser rendered as HP1009, one only in the server HTML as HP1008 and a different element in the same place as HP1007. A missing <tbody> and other repaired markup is HP3001, and <head> differences are HP1014:

npx hydration-proof test

no-invalid-interactive-nesting reports <tr> directly in <table> and block elements in <p>, and no-window-render-branch and no-storage-in-initial-render report the one-sided renders.