Hydration is a critical process in Next.js applications where the server-rendered HTML is made interactive by attaching event listeners and reconciling the DOM with the React component tree. When this process encounters inconsistencies between the server-rendered HTML and what React expects to render on the client, hydration errors occur.
Let's explore the most common hydration errors in Next.js applications and how to solve them:
This is one of the most frequent hydration errors, occurring when the text content rendered on the server differs from what's rendered on the client.
Warning: Text content did not match. Server: "Hello World" Client: "Hello User"
window
or localStorage
during rendering
// INCORRECT - Will cause hydration errors
function Greeting() {
const [name, setName] = useState("User");
useEffect(() => {
// This runs after hydration, causing a mismatch
setName(localStorage.getItem("username") || "User");
}, []);
return Hello {name}
;
}
// CORRECT - Prevents hydration errors
function Greeting() {
const [name, setName] = useState("World"); // Default value for both server and client
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
setName(localStorage.getItem("username") || "User");
}, []);
return Hello {isClient ? name : "World"}
;
}
This occurs when HTML attributes or React props differ between server and client rendering.
Warning: Prop 'style' did not match. Server: "color:blue" Client: "color:red"
// INCORRECT - Will cause hydration errors
function ColoredText() {
const [color, setColor] = useState("blue");
useEffect(() => {
// This runs after hydration, causing a mismatch
setColor(window.matchMedia("(prefers-color-scheme: dark)").matches ? "white" : "black");
}, []);
return Colored text
;
}
// CORRECT - Prevents hydration errors
function ColoredText() {
const [color, setColor] = useState("blue");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setColor(window.matchMedia("(prefers-color-scheme: dark)").matches ? "white" : "black");
}, []);
// On first render, return the server version
if (!mounted) {
return Colored text
;
}
// After hydration, return the client version
return Colored text
;
}
This error happens when the structure of the DOM differs between server and client rendering.
Warning: Expected server HTML to contain a matching <div> in <main>.
// INCORRECT - Will cause hydration errors
function MobileMenu() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
setIsMobile(window.innerWidth < 768);
}, []);
return (
);
}
// CORRECT - Prevents hydration errors
function MobileMenu() {
const [isMobile, setIsMobile] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setIsMobile(window.innerWidth < 768);
}, []);
// On first render, always use DesktopNav for both server and client
if (!mounted) {
return (
);
}
// After hydration, render based on screen size
return (
);
}
For components that rely heavily on browser APIs, you can use dynamic imports with the ssr: false
option:
// components/BrowserOnlyComponent.js
import { useEffect, useState } from 'react';
export default function BrowserOnlyComponent() {
const [browserInfo, setBrowserInfo] = useState(null);
useEffect(() => {
setBrowserInfo({
userAgent: navigator.userAgent,
language: navigator.language,
screenWidth: window.innerWidth,
});
}, []);
return (
{browserInfo && (
- User Agent: {browserInfo.userAgent}
- Language: {browserInfo.language}
- Screen Width: {browserInfo.screenWidth}px
)}
);
}
// pages/index.js
import dynamic from 'next/dynamic';
const BrowserOnlyComponent = dynamic(
() => import('../components/BrowserOnlyComponent'),
{ ssr: false }
);
export default function Home() {
return (
My App
);
}
React's useId
hook generates stable IDs that match between server and client:
import { useId } from 'react';
function FormField({ label }) {
const id = useId();
return (
);
}
For cases where differences are expected and harmless, you can use the suppressHydrationWarning
prop:
function CurrentTime() {
return (
Current time: {new Date().toLocaleTimeString()}
);
}
Note: Use this sparingly and only when the mismatch is expected and doesn't affect functionality.
React Developer Tools can help identify components causing hydration issues:
Enable Strict Mode in your Next.js application to catch potential issues:
// next.config.js
module.exports = {
reactStrictMode: true,
}
Isolate problematic components with a hydration boundary:
function HydrationBoundary({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return Loading...;
}
return <>{children}>;
}
// Usage
function MyPage() {
return (
{/* Server and client rendered */}
{/* Only client rendered */}
{/* Server and client rendered */}
);
}
In the App Router, you can mark components as client-only using the 'use client' directive:
// components/ClientComponent.js
'use client'
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [windowWidth, setWindowWidth] = useState(0);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return Window width: {windowWidth}px;
}
For cookie access in Server Components, use the cookies() function instead of document.cookie:
// app/page.js
import { cookies } from 'next/headers';
export default function Page() {
const cookieStore = cookies();
const theme = cookieStore.get('theme')?.value || 'light';
return Current theme: {theme};
}
Use the server-only and client-only packages to prevent accidental usage in the wrong environment:
// Install the packages
// npm install server-only client-only
// In a server-only file
import 'server-only';
export async function getDataFromDatabase() {
// Server-only code
}
// In a client-only file
import 'client-only';
export function useWindowSize() {
// Client-only code
}
Let React handle the DOM to ensure consistency between server and client rendering.
Always defer browser-specific logic to useEffect and use conditional rendering to handle the pre-hydration state.
Some libraries may not be SSR-compatible. Check documentation or wrap them in dynamic imports with { ssr: false }.
Ensure data used for rendering comes from consistent sources on both server and client.
Development mode may hide some hydration issues. Test your application in production mode before deploying.
Hydration errors can be frustrating, but with a proper understanding of how Next.js handles server-side rendering and client-side hydration, most issues can be resolved systematically. By following the patterns and best practices outlined in this article, you can ensure a smooth user experience with consistent rendering between server and client.
Remember that the key to avoiding hydration errors is to ensure that the initial render on the client matches exactly what was rendered on the server, and then apply client-specific changes only after hydration is complete.
With Create React App's deprecation, explore modern alternatives like Vite, Next.js, and Remix for your React projects.
Learn how to integrate ImageKit with Next.js for optimized image delivery, transformations, and improved performance.
A comprehensive guide to integrating Razorpay payment gateway in your Next.js applications with best security practices.
Get the latest articles and project management insights delivered to your inbox.