Hydration in React/Next.js
The Problem Statement
Hydration is the process where a server-rendered HTML page becomes an interactive React application on the client. You hit this problem when your Next.js page loads instantly (thanks to server-side rendering) but then suddenly flickers, loses state, or feels janky as JavaScript kicks in. Sound familiar? You’ve likely seen the dreaded “hydration mismatch” warning in your console. It happens because the static HTML the server sent doesn’t perfectly match the React tree the browser tries to build. Why does this matter? Because without proper hydration, your app can break functionality like click handlers, form inputs, or third-party widgets that rely on client-side JavaScript.
The Core Explanation
Hydration is the bridge between static HTML and dynamic React. Here’s how it works in three steps:
Server sends dry HTML: Next.js renders your React components on the server, producing plain HTML with no JavaScript. This HTML is fast to display—users see content immediately.
Browser renders that HTML: The browser paints the page. It looks complete, but nothing is interactive yet—no buttons work, no dropdowns open.
React “waters” the HTML: React’s JavaScript bundle downloads and runs. React attaches event listeners (like
onClick), initializes component state, and connects the virtual DOM to the existing DOM nodes. This is hydration—React takes the server’s static markup and brings it to life.
The catch? React expects the server-sent HTML structure to exactly match what it would render on the client. If there’s any difference—say, a useEffect that changes something on mount, or a component that uses window.innerWidth—React throws a mismatch error and re-renders the whole tree. That’s where performance issues and user-facing glitches start.
Simple analogy: Think of a pre-filled form on paper. Server sends you a printed form with all fields filled. Hydration is you taking that paper form, scanning it, and turning it into an editable digital document where you can click buttons and type. If the digital version has a different layout (extra field, missing label), the scan fails.
The Practical Context
Use hydration when you need fast initial page loads and SEO, which is almost every server-rendered Next.js app. Hydration is automatic in Next.js for pages using getServerSideProps or getStaticProps. You don’t opt in—you opt out.
Avoid hydration when your page has no dynamic content at all. If every visitor sees exactly the same static marketing page, skip React entirely and use a plain HTML/CSS approach. Also, avoid heavy hydration for pages that rely heavily on client-only data (like dashboards) – consider using next/dynamic with ssr: false to skip server rendering entirely for those components.
Real-world use cases:
- E-commerce product pages: SEO needs server-rendered content, but “Add to Cart” buttons and image galleries need hydration. Mismatches here cause cart logic to break silently.
- Blog with comments: Server renders the article; hydration enables live comment submission without a page reload. Mismatch often happens if the server shows a logout button but the client knows the user is logged in.
- Admin dashboards: You may prefer client-only rendering for authenticated pages to avoid hydration mismatch with user-specific data.
Why should you care? Hydration mismatches cause bugs that are hard to reproduce—they happen only on first load, then disappear on subsequent navigations. They degrade Core Web Vitals (Layout Shifts from re-renders) and user trust.
The Quick Example
Here’s a common hydration mismatch and how to fix it:
// ❌ Problem: uses client-only data during server render
function Greeting() {
const [name, setName] = useState('')
useEffect(() => {
// This runs only on client – server sends empty name
setName(localStorage.getItem('username'))
}, [])
return <h1>Hello, {name}</h1>
}
// ✅ Fix: ensure server output matches client initial render
function Greeting() {
const [name, setName] = useState('')
useEffect(() => {
// Same logic, but initial state is 'User' – both sides match
setName(localStorage.getItem('username') || 'User')
}, [])
return <h1>Hello, {name}</h1>
}
What this demonstrates: The server renders <h1>Hello, </h1> (empty name). The client tries to build the same, but useEffect runs and sets name to a stored value. React detects a difference ('' vs 'Alice') and throws a hydration mismatch. By initializing useState with a fallback value that matches server output, you prevent the mismatch. The useEffect will still update the UI after hydration—but without re-rendering the whole page.
The Key Takeaway
Always make your server-rendered output identical to your component’s initial client render—ensure useState default values and any conditions based on typeof window are consistent. For deeper dives, read React’s official hydration documentation.
Top comments (0)