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.
Solution 2: Fix localStorage/sessionStorage Access
Browser APIs don't exist on the server:
// ❌ BAD: localStorage doesn't exist on server
export function UserPreferences() {
const theme = localStorage.getItem('theme')
return <div className={theme}>Content</div>
}
// ✅ GOOD: Check if window exists
export function UserPreferences() {
const theme = typeof window !== 'undefined'
? localStorage.getItem('theme')
: 'light'
return <div className={theme}>Content</div>
}
// ✅ BETTER: Use useEffect
'use client'
import { useEffect, useState } from 'react'
export function UserPreferences() {
const [theme, setTheme] = useState('light')
useEffect(() => {
setTheme(localStorage.getItem('theme') || 'light')
}, [])
return <div className={theme}>Content</div>
}
Solution 3: Fix Random Values
Never use Math.random() in render:
// ❌ BAD: Different random on server vs client
export function RandomId() {
return <div id={Math.random()}>Content</div>
}
// ✅ GOOD: Generate ID in useEffect
'use client'
import { useEffect, useState } from 'react'
export function RandomId() {
const [id, setId] = useState('')
useEffect(() => {
setId(Math.random().toString(36).substr(2, 9))
}, [])
return <div id={id}>Content</div>
}
// ✅ BETTER: Use a library
import { useId } from 'react'
export function RandomId() {
const id = useId()
return <div id={id}>Content</div>
}
Solution 4: Fix Conditional Rendering
Ensure same content renders on server and client:
// ❌ BAD: Different rendering based on client state
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false)
return (
<div>
{isDark ? '🌙 Dark' : '☀️ Light'}
</div>
)
}
// Server renders: ☀️ Light
// Client renders: 🌙 Dark (from state)
// ❌ Mismatch!
// ✅ GOOD: Render same on server and client
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setIsDark(localStorage.getItem('theme') === 'dark')
setMounted(true)
}, [])
if (!mounted) {
return <div>☀️ Light</div> // Server renders this
}
return (
<div>
{isDark ? '🌙 Dark' : '☀️ Light'}
</div>
)
}
Solution 5: Fix Browser-Only APIs
Never use window/document in Server Components:
// ❌ BAD: window doesn't exist on server
export function UserAgent() {
return <div>{window.navigator.userAgent}</div>
}
// ✅ GOOD: Use 'use client' directive
'use client'
export function UserAgent() {
return <div>{window.navigator.userAgent}</div>
}
// ✅ BETTER: Use useEffect
'use client'
import { useEffect, useState } from 'react'
export function UserAgent() {
const [userAgent, setUserAgent] = useState('')
useEffect(() => {
setUserAgent(window.navigator.userAgent)
}, [])
return <div>{userAgent}</div>
}
Solution 6: Fix Prop Serialization
Props must be serializable (no functions, dates, etc.):
// ❌ BAD: Non-serializable props
export function Component({ callback }: { callback: () => void }) {
return <button onClick={callback}>Click</button>
}
// ✅ GOOD: Serialize dates to strings
export function Component({ date }: { date: string }) {
return <div>{date}</div>
}
// ✅ GOOD: Use server actions for callbacks
'use client'
export function Component({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
return (
<form action={onSubmit}>
<button type="submit">Submit</button>
</form>
)
}
Solution 7: Fix Inline Functions
Never pass inline functions as props:
// ❌ BAD: Inline function changes on every render
export function Component() {
return (
<Child onClick={() => console.log('clicked')} />
)
}
// ✅ GOOD: Define function outside
const handleClick = () => console.log('clicked')
export function Component() {
return <Child onClick={handleClick} />
}
// ✅ GOOD: Use useCallback
'use client'
import { useCallback } from 'react'
export function Component() {
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
return <Child onClick={handleClick} />
}
Solution 8: Use Dynamic Imports
For components that can't be hydrated:
import dynamic from 'next/dynamic'
// Load component without SSR
const ClientOnlyComponent = dynamic(
() => import('@/components/ClientOnly'),
{ ssr: false }
)
export default function Page() {
return (
<div>
<h1>Server Content</h1>
<ClientOnlyComponent />
</div>
)
}
Advanced Debugging
Enable React Strict Mode
// next.config.mjs
const nextConfig = {
reactStrictMode: true,
}
export default nextConfig
Add Hydration Monitoring
'use client'
import { useEffect } from 'react'
export function HydrationMonitor() {
useEffect(() => {
console.log('✅ Hydration complete')
// Monitor for mismatches
const observer = new MutationObserver(() => {
console.warn('⚠️ DOM changed after hydration')
})
observer.observe(document.body, {
childList: true,
subtree: true,
})
return () => observer.disconnect()
}, [])
return null
}
Test Hydration Locally
# Build for production
npm run build
# Start production server
npm run start
# Test in different browsers
# Test with different timezones
# Test with browser extensions disabled
Prevention Checklist
Before committing code:
□ No direct window/document access in Server Components
□ No Math.random() in render
□ No new Date() in render (use useEffect)
□ No localStorage/sessionStorage without useEffect
□ All conditional rendering is consistent
□ All props are serializable
□ No inline functions in props
□ No circular references in state
□ React Strict Mode enabled
□ Tested in production build
Common Mistakes
Mistake #1: Using suppressHydrationWarning everywhere - Only use for unavoidable cases like timestamps
Mistake #2: Not testing production builds - Hydration errors often only appear in production
Mistake #3: Ignoring timezone differences - Server and client may be in different timezones
Mistake #4: Passing functions as props - Functions aren't serializable
Mistake #5: Not using useEffect for client-only code - Browser APIs must be accessed in useEffect
FAQ
Are hydration warnings serious?
Yes! They indicate your app HTML does not 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
- Building SaaS with Next.js and Supabase
- React Server Components Deep Dive
Conclusion
Hydration mismatch errors are usually caused by inconsistent rendering between server and client. The most common causes are date/time differences, browser API access, and random values.
The key to preventing these errors is ensuring your server and client render identical HTML. Use useEffect for client-only code, suppressHydrationWarning only for unavoidable cases, and always test your production builds.
With these solutions in place, you'll eliminate hydration errors and have a more stable, performant Next.js application.
Originally published at https://iloveblogs.blog









