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

bash
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)

javascript
// 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

javascript
// 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

javascript
// 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

javascript
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

javascript
// 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

javascript
function Timestamp() {
  const [dateStr, setDateStr] = useState('');
  useEffect(() => {
    setDateStr(new Date().toLocaleString());
  }, []);
  if (!dateStr) return <time>Loading...</time>;
  return <time>{dateStr}</time>;
}

Debugging Tips

  • Add suppressHydrationWarning to elements that intentionally differ (use sparingly)
  • Compare HTML source (Ctrl+U) with rendered DOM (DevTools) to spot differences
  • Use Next.js useEffect to defer client-only rendering
  • Check for unclosed HTML tags that browsers auto-correct differently