Resolve Next.js Hydration Mismatch Errors Complete Guide
Hydration mismatch errors breaking your Next.js app? Learn the root causes and 8 proven fixes to eliminate these errors permanently.
Resolve Next.js Hydration Mismatch Errors Complete Guide#
Hydration mismatch errors are one of the most common and frustrating issues in Next.js applications. You see warnings like "Text content does not match server-rendered HTML" or "Hydration failed because the initial UI does not match what was rendered on the server." This comprehensive guide explains why these happen and provides 8 proven solutions.
This article is part of our comprehensive Deploying Next.js + Supabase to Production guide.
What is Hydration?#
Hydration is the process where React attaches event listeners to server-rendered HTML:
- Server renders HTML - Next.js generates static HTML on the server
- Browser receives HTML - User sees content immediately (fast!)
- React hydrates - JavaScript makes the page interactive
- Mismatch detected - If HTML doesn't match, React throws an error
Common Error Messages#
## Error 1: Text content mismatch
Warning: Text content did not match. Server: "Hello" Client: "Hi"
## Error 2: Prop mismatch
Warning: Prop `className` did not match. Server: "dark" Client: "light"
## Error 3: Extra/missing elements
Warning: Expected server HTML to contain a matching <div> in <div>
## Error 4: Hydration failed
Error: Hydration failed because the initial UI does not match
Solution 1: Fix Date/Time Rendering#
The #1 cause of hydration errors:
Problem: Server and Client Timezones Differ#
// ❌ BAD: Server and client render different times
export function PostDate({ date }: { date: Date }) {
return <time>{date.toLocaleString()}</time>
// Server (UTC): "2/16/2026, 10:00:00 AM"
// Client (PST): "2/16/2026, 2:00:00 AM"
// ❌ Hydration mismatch!
}
Solution: Use suppressHydrationWarning#
// ✅ GOOD: Suppress warning for time elements
export function PostDate({ date }: { date: Date }) {
return (
<time suppressHydrationWarning>
{date.toLocaleString()}
</time>
)
}
Solution: Client-Side Only Rendering#
'use client'
import { useEffect, useState } from 'react'
export function PostDate({ date }: { date: Date }) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
// Return server-safe fallback
return <time>{date.toISOString().split('T')[0]}</time>
}
// Client-side rendering
return <time>{date.toLocaleString()}</time>
}
Solution: Use UTC Consistently#
// ✅ BEST: Format in UTC on both server and client
export function PostDate({ date }: { date: Date }) {
const formatted = new Intl.DateTimeFormat('en-US', {
timeZone: 'UTC',
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date)
return <time>{formatted}</time>
}
Solution 2: Fix localStorage/sessionStorage Access#
Browser APIs don't exist on the server:
Problem: Accessing localStorage During Render#
// ❌ BAD: localStorage accessed during render
export function ThemeToggle() {
const theme = localStorage.getItem('theme') || 'light'
// ❌ ReferenceError: localStorage is not defined (server)
return <button>{theme}</button>
}
Solution: Use useEffect Hook#
'use client'
import { useEffect, useState } from 'react'
export function ThemeToggle() {
const [theme, setTheme] = useState('light') // Default value
useEffect(() => {
// Only runs on client
const savedTheme = localStorage.getItem('theme') || 'light'
setTheme(savedTheme)
}, [])
return <button>{theme}</button>
}
Solution: Create Custom Hook#
// hooks/useLocalStorage.ts
'use client'
import { useEffect, useState } from 'react'
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(initialValue)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
try {
const item = window.localStorage.getItem(key)
if (item) {
setValue(JSON.parse(item))
}
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error)
}
}, [key])
const setStoredValue = (newValue: T) => {
try {
setValue(newValue)
window.localStorage.setItem(key, JSON.stringify(newValue))
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
return [mounted ? value : initialValue, setStoredValue] as const
}
// Usage
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme}
</button>
)
}
Solution 3: Fix Random Values#
Random values differ between server and client:
Problem: Math.random() or uuid()#
// ❌ BAD: Random ID generated on server and client
export function RandomComponent() {
const id = Math.random().toString(36)
// Server: "0.abc123"
// Client: "0.xyz789"
// ❌ Hydration mismatch!
return <div id={id}>Content</div>
}
Solution: Generate ID on Client Only#
'use client'
import { useId } from 'react'
export function RandomComponent() {
// React's useId generates consistent IDs
const id = useId()
return <div id={id}>Content</div>
}
Solution: Pass ID as Prop#
// Generate ID on server, pass as prop
export function ParentComponent() {
const id = crypto.randomUUID()
return <RandomComponent id={id} />
}
export function RandomComponent({ id }: { id: string }) {
return <div id={id}>Content</div>
}
Solution 4: Fix Conditional Rendering#
Different conditions on server vs client:
Problem: window Object Checks#
// ❌ BAD: window check causes mismatch
export function ResponsiveComponent() {
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
// Server: false (window undefined)
// Client: true (if mobile)
// ❌ Hydration mismatch!
return <div>{isMobile ? 'Mobile' : 'Desktop'}</div>
}
Solution: Use CSS Media Queries#
// ✅ GOOD: CSS handles responsiveness
export function ResponsiveComponent() {
return (
<>
<div className="block md:hidden">Mobile</div>
<div className="hidden md:block">Desktop</div>
</>
)
}
Solution: Client-Side Detection#
'use client'
import { useEffect, useState } from 'react'
export function ResponsiveComponent() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768)
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
// Render same content on server and initial client render
return <div>{isMobile ? 'Mobile' : 'Desktop'}</div>
}
Solution 5: Fix Third-Party Libraries#
Some libraries cause hydration issues:
Problem: Libraries with Browser-Only Code#
// ❌ BAD: Chart library runs on server
import Chart from 'some-chart-library'
export function ChartComponent() {
return <Chart data={data} />
// ❌ May cause hydration errors
}
Solution: Dynamic Import with ssr: false#
import dynamic from 'next/dynamic'
// ✅ GOOD: Load only on client
const Chart = dynamic(() => import('some-chart-library'), {
ssr: false,
loading: () => <div>Loading chart...</div>,
})
export function ChartComponent() {
return <Chart data={data} />
}
Solution: Wrap in Client Component#
// components/ChartWrapper.tsx
'use client'
import Chart from 'some-chart-library'
export function ChartWrapper({ data }) {
return <Chart data={data} />
}
// page.tsx (Server Component)
import { ChartWrapper } from '@/components/ChartWrapper'
export default function Page() {
return <ChartWrapper data={data} />
}
Solution 6: Fix HTML Structure Issues#
Invalid HTML nesting causes hydration errors:
Problem: Invalid Nesting#
// ❌ BAD: <p> cannot contain <div>
export function BadNesting() {
return (
<p>
<div>This is invalid HTML</div>
</p>
)
}
// ❌ BAD: <button> cannot contain <button>
export function NestedButtons() {
return (
<button>
<button>Click</button>
</button>
)
}
Solution: Fix HTML Structure#
// ✅ GOOD: Valid HTML nesting
export function GoodNesting() {
return (
<div>
<p>Paragraph text</p>
<div>Div content</div>
</div>
)
}
// ✅ GOOD: No nested buttons
export function NoNestedButtons() {
return (
<div>
<button onClick={handleClick}>Click</button>
</div>
)
}
Solution 7: Fix Browser Extension Interference#
Browser extensions can modify HTML:
Problem: Extensions Inject Code#
// Extensions like Grammarly, LastPass, etc. inject HTML
// This causes hydration mismatches
Solution: Add suppressHydrationWarning to Root#
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body suppressHydrationWarning>{children}</body>
</html>
)
}
Solution: Detect and Handle Extensions#
'use client'
import { useEffect, useState } from 'react'
export function ExtensionSafeComponent() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (!isClient) {
return <div>Loading...</div>
}
return <div>Content safe from extensions</div>
}
Solution 8: Debug Hydration Errors#
Find the exact source of errors:
Enable Detailed Errors#
// next.config.mjs
const nextConfig = {
reactStrictMode: true, // Shows hydration errors in development
}
Use React DevTools#
## Install React DevTools browser extension
## Enable "Highlight updates" to see hydration issues
Add Error Boundaries#
'use client'
import { Component, ReactNode } from 'react'
export class HydrationErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean }
> {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Hydration error:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please refresh.</div>
}
return this.props.children
}
}
Common Mistakes#
-
Mistake #1: Ignoring warnings - Hydration warnings indicate real bugs
-
Mistake #2: Using suppressHydrationWarning everywhere - Only use for unavoidable cases
-
Mistake #3: Not testing with extensions disabled - Extensions can hide real issues
-
Mistake #4: Accessing window during render - Use useEffect for browser APIs
-
Mistake #5: Different server/client logic - Keep rendering logic consistent
FAQ#
Are hydration warnings serious?#
Yes! They indicate your app's HTML doesn't match between server and client, which can cause bugs, poor performance, and SEO issues.
Can I just suppress all warnings?#
No. suppressHydrationWarning should only be used for unavoidable cases like timestamps. Fix the root cause instead.
Why do hydration errors only appear sometimes?#
They depend on timing, browser extensions, and environment differences. Test thoroughly in production-like environments.
Do hydration errors affect SEO?#
Yes. Search engines see the server-rendered HTML, but users might see different content after hydration, confusing search engines.
How do I test for hydration errors?#
Enable React Strict Mode, test with different timezones, disable browser extensions, and test on different devices.
Related Articles#
- Next.js Turbopack Stuck on Compiling How to Fix
- Fix Next.js Build Error Module Not Found After Deploy
- Deploy Next.js 15 to Vercel Without Environment Variable Errors
- Next.js Performance Optimization: 10 Essential Techniques
Conclusion#
Hydration mismatch errors occur when server-rendered HTML doesn't match client-rendered HTML. The most common causes are date/time formatting, localStorage access, random values, and conditional rendering based on browser APIs.
Fix these by using suppressHydrationWarning sparingly, moving browser API access to useEffect, using consistent UTC formatting for dates, and ensuring HTML structure is valid. Always test with React Strict Mode enabled and browser extensions disabled.
Remember: hydration warnings are not just cosmetic issues—they indicate real bugs that can affect user experience, performance, and SEO. Take the time to fix them properly rather than suppressing them.
Frequently Asked Questions
Continue Reading
Next.js 15 vs Next.js 14: Performance Comparison and Migration Guide 2026
Comprehensive comparison of Next.js 15 and 14 performance improvements, new features, breaking changes, and whether you should upgrade your production app.
Fix Next.js Build Error Module Not Found After Deploy
Module not found errors only in production? Learn why Next.js builds fail after deploy and get 6 proven fixes that work on Vercel, AWS, and other platforms.
Next.js Turbopack Stuck on Compiling How to Fix
Turbopack stuck on compiling in Next.js 15? Learn the exact causes and 5 proven fixes to get your dev server running in minutes.
Browse by Topic
Find stories that matter to you.