ProjectsCraftsFigmaBlogsContact

Understanding Hydration in Next.js: A Simple Guide

December 9, 2025•4 min read•Piyush Zingade
#nextjs#react#hydration#app-router

Understanding Hydration in Next.js

If you've been working with Next.js, you've probably run into this frustrating error at some point:

"Hydration failed because the server-rendered HTML did not match the client..."

Don't worry, you're not alone. This is one of the most common issues developers face when building Next.js applications. Let me help you understand what's happening and how to fix it.

What is Hydration?

Think of hydration as React bringing your page to life. Here's how it works:

First, the server creates a complete HTML page and sends it to your browser. This happens really fast, so users see content immediately. But at this point, the page is just static HTML - nothing is interactive yet.

Then, React takes over in the browser. It "hydrates" the static HTML by attaching all the JavaScript functionality - event listeners, state management, and everything else that makes your app interactive.

Here's the catch: The HTML that the server creates must match exactly what React expects to see when it hydrates. If there's any mismatch, you get a hydration error.

Server Components vs Client Components

In Next.js 16 with the App Router, there's an important concept to understand: by default, everything is a Server Component.

Server Components run only on the server. They're great for fetching data and rendering content, but they can't access browser-specific things like window or localStorage. They also don't include any JavaScript in the bundle sent to the browser, which makes your app faster.

Client Components run in the browser. You need these when you want interactivity - things like buttons that respond to clicks, forms that update as you type, or anything that uses React hooks like useState or useEffect. To create a Client Component, just add 'use client' at the top of your file.

Common Mistakes and How to Fix Them

Problem 1: Trying to Use Browser APIs in Server Components

This is probably the most common mistake. You try to access window, document, or localStorage in a component, but these don't exist on the server.

Here's what doesn't work:

export default function Page() {
  const width = window.innerWidth // Error: window is not defined
  return <div>Width: {width}</div>
}

Here's the fix:

Move your code to a Client Component and use useEffect to access browser APIs only after the component mounts:

'use client'
import { useState, useEffect } from 'react'

export default function WindowWidth() {
  const [width, setWidth] = useState(0)

  useEffect(() => {
    // This only runs in the browser
    setWidth(window.innerWidth)
  }, [])

  return <div>Width: {width}</div>
}

Problem 2: Random or Time-Based Values

If you generate random numbers or use the current time during rendering, the server and client will produce different values, causing a mismatch.

This causes problems:

export default function Page() {
  return <div>Random: {Math.random()}</div> // Server gets one number, client gets another
}

Here's the solution:

Generate these values only on the client side using useEffect:

'use client'
import { useState, useEffect } from 'react'

export default function RandomNumber() {
  const [num, setNum] = useState<number | null>(null)

  useEffect(() => setNum(Math.random()), [])

  return <div>{num}</div>
}

Problem 3: Invalid HTML Structure

Browsers are strict about HTML rules. If you nest elements incorrectly, you'll get hydration errors because the browser will automatically fix the HTML, creating a mismatch.

This is invalid HTML:

<p>
  <div>You can't put a div inside a paragraph</div>
</p>

Use proper nesting instead:

<div>
  <div>This works perfectly</div>
</div>

Key Takeaways

Let me leave you with a few important points to remember:

Start with Server Components. They're faster and more efficient. Only switch to Client Components when you actually need browser APIs or interactivity.

Use 'use client' sparingly. Add it only to components that need onClick handlers, useState, useEffect, or browser APIs.

useEffect is your best friend. Whenever you need to run code that's browser-specific, put it inside useEffect. This ensures it only runs on the client, avoiding hydration mismatches.

When all else fails, you can use the suppressHydrationWarning prop on an element, but this should be a last resort. It's better to fix the underlying issue than to hide the warning.

Understanding hydration takes a bit of practice, but once you get the hang of it, these errors become much easier to debug and prevent.

0
likes