The Problem
When using server-side rendering (SSR) with React or Next.js, the server generates HTML and sends it to the browser. React then "hydrates" this HTML by attaching event listeners. If the client-side render produces different HTML than the server, you get a hydration mismatch error.
Symptoms
- Console warning: "Hydration failed because the server rendered HTML didn't match the client"
- UI flickers after page load as React re-renders
- Event handlers do not work on mismatched elements
- Random content appears differently between server and client
- Next.js shows overlay error in development
Real Error Message
Warning: Hydration failed because the initial UI does not match what was
rendered on the server.
+ Expected: <div>Hello World</div>
- Received: <div>Hello User</div>Common Causes and Fixes
Cause 1: Browser-Only APIs (window, localStorage)
// BAD: window is undefined on the server
function ThemeToggle() {
const [theme, setTheme] = useState(
window.localStorage.getItem('theme') || 'light'
);
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>;
}Fix: Use useEffect for browser-only code
```javascript function ThemeToggle() { const [theme, setTheme] = useState('light'); const [mounted, setMounted] = useState(false);
useEffect(() => { setTheme(localStorage.getItem('theme') || 'light'); setMounted(true); }, []);
if (!mounted) return <button>light</button>; // Match server output return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>; } ```
Cause 2: Random Values and Timestamps
// BAD: Different random value on server vs client
function LotteryNumber() {
return <span>Your number: {Math.floor(Math.random() * 100)}</span>;
}Fix: Generate on client only
```javascript function LotteryNumber() { const [number, setNumber] = useState(null);
useEffect(() => { setNumber(Math.floor(Math.random() * 100)); }, []);
if (number === null) return <span>Your number: ...</span>; return <span>Your number: {number}</span>; } ```
Cause 3: Conditional Rendering Based on Auth State
// BAD: Server doesn't know auth state
function Dashboard() {
const { user } = useAuth();
if (!user) return <Login />;
return <AdminPanel />;
}Fix: Use Next.js getServerSideProps for server-side auth check
export async function getServerSideProps(context) {
const session = await getSession(context.req);
if (!session) {
return { redirect: { destination: '/login', permanent: false } };
}
return { props: { user: session.user } };
}Cause 4: Date/Time Formatting
// BAD: Server timezone may differ from client
function Timestamp() {
return <time>{new Date().toLocaleString()}</time>;
}Fix: Pass timezone from server or render client-side only
function Timestamp() {
const [dateStr, setDateStr] = useState('');
useEffect(() => {
setDateStr(new Date().toLocaleString());
}, []);
if (!dateStr) return <time>Loading...</time>;
return <time>{dateStr}</time>;
}Debugging Tips
- Add
suppressHydrationWarningto elements that intentionally differ (use sparingly) - Compare HTML source (Ctrl+U) with rendered DOM (DevTools) to spot differences
- Use Next.js
useEffectto defer client-only rendering - Check for unclosed HTML tags that browsers auto-correct differently