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":
| Warning | The first tag is | Meaning |
|---|---|---|
Expected server HTML to contain a matching <X> in <Y> | In the browser render only | The 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 only | The 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
-
Match the tags to your code. The warning names the child and the parent. The component stack names the component.
-
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> ); } -
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>; } -
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 testno-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.