
How to Fix Next.js Hydration Errors: Text Content Did Not Match Guide
If you build web applications using React-based server-side rendering (SSR) frameworks like Next.js, Remix, or Astro, you have likely encountered this warning: "Hydration failed because the initial UI does not match what was rendered on the server." or "Text content did not match. Server: '...' Client: '...'".
This error occurs when the pre-rendered static HTML sent by the server differs from the initial DOM tree generated by React on the client.
In this guide, we will analyze what hydration is, explore common causes of hydration mismatch errors, and implement reliable fixes.
What is Hydration?
To deliver fast page load speeds and SEO indexing, server-side rendering works in two phases:
- Server Phase (Pre-rendering): The node server compiles your React component tree into raw static HTML strings and sends them to the client browser. The user sees the page layout instantly, but it is static and non-interactive.
- Client Phase (Hydration): The browser downloads your JavaScript bundles. React parses the components, binds interactive event handlers (like onClick listeners) to the existing HTML, and makes the page functional.
For hydration to succeed, the server HTML and client-side JavaScript output must align exactly, down to the last text character and DOM element type. If there is a single tag or text mismatch, React throws a hydration warning.
Common Causes and How to Resolve Them
Let's examine the three most common developers errors that trigger hydration failures.
Cause 1: Invalid HTML Structure Nesting
Browsers are highly opinionated about HTML semantics. If you write illegal DOM nesting (such as putting a block-level <div> inside a paragraph <p>, or missing <tbody> tags inside <table>), the browser's HTML parser silently restructures the DOM.
For example, if you write this:
// INVALID: Paragraph tag cannot nest div container
export function BadComponent() {
return (
<p>
Welcome!
<div>More text...</div>
</p>
);
}The server generates raw HTML matching your layout. However, when the browser parses it, it auto-closes the <p> tag early when it encounters the <div>.
When React attempts to hydrate, it scans the browser's modified DOM and finds an unexpected layout mismatch, triggering a hydration crash.
- The Fix: Ensure your layouts comply with strict HTML nesting rules. Use the browser's "Inspect Element" tool to verify that the final parsed DOM matches your React code structure.
Cause 2: Referencing Client-Only API Globals
If you reference client-only variables (like window, document, or localStorage) during component initialization, the server has no access to these variables, resulting in undefined values during server-side compilation.
// INVALID: window is undefined on the Node.js server
export function WindowWidth() {
const width = window.innerWidth;
return <div>Width: {width}</div>;
}The server outputs <div>Width: </div>. But on the client, window.innerWidth evaluates to 1920, rendering <div>Width: 1920</div>. The resulting text mismatch causes hydration to fail.
- The Fix: Wrap client-only checks inside a
useEffecthook. SinceuseEffectonly executes after the component mounts on the client browser, it bypasses the initial server rendering pass.
import { useState, useEffect } from 'react';
export function WindowWidth() {
const [width, setWidth] = useState<number | null>(null);
useEffect(() => {
// Executes safely only in the browser context
setWidth(window.innerWidth);
}, []);
if (width === null) return <div>Loading...</div>;
return <div>Width: {width}px</div>;
}Cause 3: Dynamic Data (Dates and Random Numbers)
Using functions like new Date() or Math.random() during render cycles creates instant discrepancies between the server compilation time and the client rendering time.
- The Fix: If you must display dates, format them inside a
useEffecthook after mounting, or suppress hydration matching using thesuppressHydrationWarningattribute (use this sparingly, as it hides errors from developers):
// Suppresses the warning for this element only
<span suppressHydrationWarning>{new Date().toLocaleTimeString()}</span>Next.js Workaround: Disable SSR for Specific Components
In Next.js, you can bypass SSR for dynamic components entirely using dynamic imports:
import dynamic from 'next/dynamic';
// Import client component dynamically, disabling server-side pre-rendering
const ClientOnlyWidget = dynamic(() => import('./Widget'), {
ssr: false,
});Conclusion
Hydration mismatches are caused by differences between server pre-renders and client DOMs. To eliminate these errors, maintain strict HTML tag nesting semantics, wrap browser-only globals (like window and localStorage) inside useEffect wrappers, avoid rendering dynamic dates during hydration passes, and utilize dynamic imports to isolate client-only widgets.