Fixing Hydration Errors in Next.js: A Structural Approach
Hydration errors are the bane of Next.js developers. Stop fighting the warnings and learn the structural causes behind 'Text content did not match server-rendered HTML'.
The Root Cause
Hydration is the process where React attaches event listeners to the static HTML sent by the server. If the DOM tree expected by React (based on its initial client render) doesn't exactly match the HTML tree from the server, React throws a hydration error and throws away the server HTML, re-rendering from scratch.
This destroys the performance benefits of SSR.
Common Culprits
1. Invalid HTML Nesting
React uses the browser's DOM parser. If your server sends invalid HTML, the browser will auto-correct it before React hydrates.
// INVALID: You cannot nest a <div> inside a <p>
export default function BadComponent() {
return (
<p>
<div>This will cause a hydration error.</div>
</p>
);
}
The browser converts this to <p></p><div>...</div>. When React hydrates, it looks inside the <p> for the <div> and panics because it's empty.
2. Time and Dates
If you render new Date().toLocaleTimeString(), the server renders it at 10:00:00, and the client hydrates at 10:00:01. Mismatch.
3. Window and LocalStorage
Checking typeof window !== 'undefined' during the initial render loop will return false on the server and true on the client.
The Universal Fix
If you have a component that must render differently on the client (like checking localStorage for a theme), you must delay that rendering until after the initial hydration pass.
export function ClientOnly({ children }: { children: React.ReactNode }) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null; // or a skeleton loader
}
return <>{children}</>;
}
By returning null on the first pass, both the server and client agree. Then, useEffect fires, triggering a safe re-render on the client only.
Tags
Related Blogs
Migrating from TanStack Start to Next.js App Router: An Architecture Post-Mortem
A deep dive into why we moved our entire CMS away from Vite SSR and TanStack Router, the performance implications of Server Components, and the hydration traps we had to fix.
Debugging SSR Runtime Crashes on Vercel: Node vs Edge
When your Next.js app works perfectly on localhost but throws 500s on Vercel. A deep dive into Node runtimes, the Edge network, and how to debug elusive server-side crashes.
Server Components vs Client Components in 60 Seconds
Still confused by the Next.js App Router? Here is the mental model you need.