Understanding Hydration Errors

5 min read

If you've worked with Next.js, you've probably hit a hydration error. The error message is not exactly helpful at first glance.

Understanding hydration errors is actually a great way to learn how React hydration works, and how Next.js splits logic between server and client.

Let's break it down.

Table of Contents

What Is Hydration?

Hydration is when React wakes up static HTML and makes it interactive.

Your server sends HTML to the browser first, but it's just markup. No clicks work, no state updates happen. It's like a screenshot of your app.

React then hydrates this HTML by attaching event listeners and state. If the server HTML doesn't match what React expects, you get a hydration error. ⚠️

Let's take a closer look.

Step 1: Server-Rendered HTML

HTML is generated on the server and sent to the browser.

STATIC
<div>Count: 0</div>
<button>Increment</button>
// No JavaScript attached

The page is visible but not yet interactive.

Step 2: Before Hydration

HTML is rendered, but React hasn't taken control yet.

LOADING
<div>Count: 0</div>
<button>Increment</button>
// React bundle loading...

Event handlers and state management are not connected.

Step 3: After Hydration

React attaches event listeners and hydrates the app.

INTERACTIVE
<div>Count: 0</div>

// onClick, useState active

Now it's a fully interactive with working state management.

Why Mismatches Happen

React doesn't just tweak the server HTML. It compares what the server generated with what it would generate itself.

The core issue is dynamic values:

<div>{Date.now()}</div>

The server might return 1694304000000, while the client returns 1757452742467.

React expects same code to produce the same result. When it finds differences, it throws an error.

Other common causes:

  • Browser-only APIs (window, localStorage)
  • Random values (Math.random())
  • User-specific data that differs between server and client
  • Conditional rendering based on client state

The Error Message

Here's what you'll see:

Error: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties...

This means what you rendered on the server isn't what React expected on the client.

For common pitfalls and solutions, see the official Next.js documentation on hydration errors.

Components and Actions in Next.js

Understanding Next.js component types helps prevent hydration errors.

Server Components ⚙️

Server Components are the default in Next.js 13+.

  • Run only on the server
  • Can fetch data, talk to databases directly
  • No useState, useEffect, or browser APIs
  • Not included in JavaScript bundle

Use for: Static content, data fetching, layouts

Client Components 💻

Client Components run on both server (for SSR) and browser.

  • Need "use client" directive at the top
  • Support hooks and browser APIs (but only after hydration)
  • Required for any interactivity
  • Included in JavaScript bundle

Use for: Interactive UI elements, state management, event handlers

Server Actions ⚡

Server Actions are functions, not components, that always run on the server.

  • Need "use server" directive
  • Work seamlessly with async/await
  • Callable from both client and server components

Use for: Database operations, form handling, server-side logic


📝 Note: A JavaScript bundle is the JS file sent to the browser. When Next.js builds the app, all Client Component code gets combined and compressed into one or more files. Server Components are excluded from this bundle, which keeps the download size smaller.


A Safe Pattern

Here's how to structure your components to avoid hydration errors.

Server Component: Pure Data Fetching

ProductPage() only fetches data and renders stable HTML.

// app/products/[id]/page.tsx
import { fetchProduct } from '@/lib/db';
import AddToCart from '@/components/AddToCart';
 
export default async function ProductPage({ params }) {
  const product = await fetchProduct(params.id);
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} />
    </div>
  );
}

Client Component: Pure Interactivity

AddToCart() handles user interactions in the browser.

// app/components/AddToCart.tsx
'use client';
import { useState } from 'react';
import { addToCart } from '@/actions/cart';
 
export default function AddToCart({ productId }) {
  const [qty, setQty] = useState(1);
  const [loading, setLoading] = useState(false);
 
  const handleSubmit = async () => {
    setLoading(true);
    await addToCart(productId, qty);
    setLoading(false);
  };
 
  return (
    <div>
      <button onClick={() => setQty(q => q + 1)}>
        Quantity: {qty}
      </button>
      <button onClick={handleSubmit} disabled={loading}>
        {loading ? 'Adding...' : 'Add to Cart'}
      </button>
    </div>
  );
}

Server Action: Pure Server Logic

addToCart() runs exclusively on the server.

// app/actions/cart.ts
'use server';
 
export async function addToCart(productId: string, qty: number) {
  await db.cart.create({
    productId,
    quantity: qty,
    userId: await getCurrentUserId()
  });
}

By separating data fetching (server), interactivity (client), and server-side mutations (actions), you can avoid the mismatches that cause hydration errors.

Takeaways

Hydration is the process of turning static HTML into an interactive React app. Errors happen when server HTML does not match client expectations.

Server Components handle data fetching and stable rendering without browser APIs. Client Components handle interactivity and state, and require the "use client" directive. Server Actions are server-only functions for mutations.

Once you understand hydration, Next.js rendering patterns become much clearer. The key is thinking about where your code runs and when it might produce different results.