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?
- Why Mismatches Happen
- The Error Message
- Components and Actions in Next.js
- A Safe Pattern
- Takeaways
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.
<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.
<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.
✨ ✨
// 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.