How to Fix Hydration Errors in Next.js: "Text content did not match server-rendered HTML"

If you have been building with Next.js for more than a week, you have inevitably encountered the dreaded red error screen: "Error: Text content did not match server-rendered HTML."
This is a Hydration Error, and it is one of the most highly searched issues by React developers transitioning to Server-Side Rendering (SSR). Today, we are going to break down exactly why this happens and how to fix it cleanly without sacrificing performance.
Why Do Hydration Errors Happen?
In Next.js, a page is first rendered on the server into static HTML. This HTML is sent to the browser so the user sees the page instantly. Then, React loads in the background and "hydrates" that HTML, turning it into an interactive application.
A hydration mismatch occurs when the HTML generated by the server differs from the HTML React expects to render on the client. The most common culprits are:
- Using
windoworlocalStoragedirectly in your component render. - Rendering dates or times (the server's time zone differs from the user's browser time zone).
- Invalid HTML nesting (like putting a
<div>inside a<p>tag).
Solution 1: The useEffect Mounting Trick
If you need to access the window object or localStorage, you must wait until the component has mounted on the client. Here is the cleanest pattern to handle this:
'use client';
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [mounted, setMounted] = useState(false);
const [theme, setTheme] = useState('light');
// useEffect only runs on the client, after the initial hydration
useEffect(() => {
setMounted(true);
setTheme(window.localStorage.getItem('theme') || 'light');
}, []);
// Return a skeleton or null during SSR to perfectly match the server HTML
if (!mounted) {
return <div className="w-8 h-8 bg-slate-200 animate-pulse rounded-full" />;
}
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current Theme: {theme}
</button>
);
}Solution 2: suppressHydrationWarning
Sometimes, a mismatch is unavoidable and harmless—like rendering a timestamp that might differ by a millisecond between the server and the client. You can explicitly tell React to ignore the warning on a specific HTML element.
export default function CurrentTime() {
const time = new Date().toLocaleTimeString();
return (
// Suppresses hydration warnings only for this specific element
<time suppressHydrationWarning>
{time}
</time>
);
}Solution 3: Dynamic Imports with ssr: false
If you have a heavy client-side component (like a map, a rich text editor, or a complex chart) that relies heavily on browser APIs, you can tell Next.js to skip server-rendering it entirely.
import dynamic from 'next/dynamic';
// This component will only ever render on the client
const ClientOnlyChart = dynamic(() => import('@/components/HeavyChart'), {
ssr: false,
loading: () => <p>Loading chart...</p>
});
export default function Dashboard() {
return (
<section>
<h1>Analytics</h1>
<ClientOnlyChart />
</section>
);
}By understanding the boundary between the server and the browser, hydration errors become a minor annoyance rather than a project-blocking nightmare.
Need help implementing this?
I partner with founders and technical teams to architect scalable, high-performance solutions.






