Hydration Proof

Search documentation

Find a page or section

Hydration failed because the initial UI does not match what was rendered on the server

React 18's hydration error, and the warning that tells you where it is.

"Hydration failed because the initial UI does not match what was rendered on the server" means React 18 found a different element in the first browser render. Find the warning just before this error: it often names the changed element and its parent.

The error

React 18 may print three messages for the same problem:

Warning: Expected server HTML to contain a matching <div> in <div>.
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

Next.js 13 and 14 add a link to the error:

Error: Hydration failed because the initial UI does not match what was rendered on the server.
See more info here: https://nextjs.org/docs/messages/react-hydration-error

Production uses a short code:

Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

React 19 renamed it to "Hydration failed because the server rendered HTML didn't match the client" and added a diff.

What hydration failed because the initial UI does not match what was rendered on the server means

React 18 compares the server HTML with the first browser render. The React 18 "Hydration failed because the initial UI does not match" error points to a structural difference. Text has a separate message: "Text content does not match server-rendered HTML" instead.

React may then render a Suspense boundary or the whole root again. The follow-up message describes that recovery: "There was an error while hydrating". The page may still work, but content can flash and input typed before hydration can be lost.

Use the earlier warning to locate the element:

WarningMeaning
Expected server HTML to contain a matching <div> in <div>The browser rendered a <div> the server HTML does not have there
Did not expect server HTML to contain a <div> in <div>The server HTML has a <div> the browser did not render
validateDOMNesting(...): <div> cannot appear as a descendant of <p>Invalid nesting that the browser rewrote before React ran

After fixing one mismatch, reload and check for another.

Next.js 13 and 14 apps using React 18 show this wording in either router. If you're searching for "NextJS 13 hydration failed because the initial UI does not match," use the preceding warning to find the element first.

Common causes

Look for a different element on the server and in the browser:

  • Invalid HTML nesting. A <div> inside a <p>, or a <tr> without <tbody>. The browser repairs the server HTML while parsing it, so React finds a different tree. See invalid HTML nesting.
  • A typeof window branch. typeof window !== 'undefined' ? <A /> : <B /> renders <B /> on the server and <A /> in the browser. See browser-only APIs.
  • Screen size, theme or storage read during render. A mobile menu chosen from window.innerWidth, a logged-in header chosen from localStorage. See media queries, theme and storage.
  • Different data. The browser fetched the list again and got one more item. See server and client data.
  • Something edited the HTML first. A browser extension, a third-party script or a CDN that minifies HTML. See browser extensions, third-party scripts and CDNs.

How to fix it

  1. Find the first warning in development. Read the element name and component stack.

  2. Identify the cause using the list above.

  3. Render the same elements on both sides. In this example, the server always chooses desktop navigation:

    components/nav.tsx
    "use client";
     
    import { DesktopNav, MobileNav } from "./navs";
     
    export function Nav() {
      const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
      return isMobile ? <MobileNav /> : <DesktopNav />;
    }

    Render stable markup and let CSS choose the visible version:

    components/nav.tsx
    import { DesktopNav, MobileNav } from "./navs";
     
    export function Nav() {
      return (
        <>
          <div className="md:hidden">
            <MobileNav />
          </div>
          <div className="hidden md:block">
            <DesktopNav />
          </div>
        </>
      );
    }

    For browser-only values, see two-pass rendering.

  4. Reload and check again.

  5. Use Suspense boundaries where appropriate. They can contain the effect of a future failure, but do not fix this one.

Find every instance

hydration-proof checks routes in a browser and can identify the changed element in a production build:

npx hydration-proof test

The report uses HP1007 for a different element, HP1008 or HP1009 for a missing element, and HP3001 for repaired HTML nesting. A re-rendered root appears as HP1011.

The ESLint plugin flags the usual sources while you type: no-window-render-branch, no-match-media-in-render, no-storage-in-initial-render and no-invalid-interactive-nesting.