solution enforces server-client parity while maintaining full client-side interactivity.
Solution 1: Fix Date/Time Rendering
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>
}
Pitfall Guide
- Direct Browser API Access in Render Phase: Accessing
window, document, localStorage, or sessionStorage synchronously during component render breaks SSR parity. Always defer to useEffect or client-only boundaries.
- Timezone & Locale Inconsistencies: Using
toLocaleString() or Date.prototype methods without explicit timeZone configuration causes server/client divergence. Always standardize on UTC or explicit locale strings.
- Non-Deterministic ID Generation:
Math.random() or client-side uuid() libraries produce different values per environment. Use React's useId() for accessibility IDs, or generate deterministic IDs server-side and pass as props.
- Overusing
suppressHydrationWarning: This attribute masks mismatches but does not resolve them. Use it only for intentionally divergent content (e.g., timestamps, ads, analytics pixels), never as a blanket fix for architectural flaws.
- JavaScript-Driven Responsiveness: Relying on
window.innerWidth or matchMedia during render causes layout shifts and hydration errors. Prefer CSS media queries for layout decisions; use JS only for interactive state after mount.
- Missing Client-Side Fallbacks: When deferring to client-side rendering, failing to provide a server-safe initial state causes FOUC or broken layouts. Always render a deterministic placeholder until
mounted state flips to true.
- Ignoring React Strict Mode: Development environments run effects twice in Strict Mode, which can expose race conditions in hydration logic. Test hydration fixes in production builds (
next build && next start) to validate real-world behavior.
Deliverables
π¦ SSR Hydration Blueprint
- Architecture diagram mapping server-safe render boundaries, client-side hydration hooks, and deterministic data flow
- Component isolation strategy for browser-dependent features
- Performance profiling checklist for hydration latency optimization
β
Pre-Deployment Hydration Checklist
βοΈ Configuration Templates
next.config.js strict mode & react strict mode alignment
- ESLint plugin configuration (
eslint-plugin-react-hooks) for render-phase API detection
- TypeScript type definitions for safe browser API access
- Custom hook templates (
useMounted, useLocalStorage, useMediaQuery) with SSR-safe defaults