I spent my entire Saturday debugging hydration errors after upgrading to Next.js 15.
What you'll fix: Those annoying "Text content does not match server-rendered HTML" errors
Time needed: 20 minutes to identify and fix most issues
Difficulty: You need basic Next.js knowledge but I'll walk through everything
Here's the exact troubleshooting method that saved my weekend (and my sanity).
Why I Built This Guide
My team upgraded 3 production apps to Next.js 15 last month. Every single one broke with hydration errors.
My setup:
- Next.js 15.0.3 with App Router
- React 19 (the new concurrent features changed things)
- TypeScript with strict mode
- Multiple dynamic components with server/client rendering
What didn't work:
- Generic Stack Overflow answers (too outdated)
- Official docs (didn't cover the specific React 19 changes)
- Disabling SSR everywhere (kills performance)
I wasted 6 hours before finding the real patterns behind these errors.
The 5 Most Common Next.js 15 Hydration Errors
Error 1: Dynamic Content Without Suppressions
The problem: Date/time components that render differently on server vs client
My solution: Use suppressHydrationWarning strategically
Time this saves: 30 minutes of head-scratching per component
// ❌ This breaks hydration every time
export function CurrentTime() {
const now = new Date().toLocaleString()
return <div>Current time: {now}</div>
}
// ✅ This works perfectly
export function CurrentTime() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <div>Loading time...</div>
}
return (
<div suppressHydrationWarning>
Current time: {new Date().toLocaleString()}
</div>
)
}
What this does: Prevents hydration checks on content that will always differ
Expected output: No more console errors about time mismatches
Before: Red hydration errors. After: Clean console - took 2 minutes to fix
Personal tip: "Only use suppressHydrationWarning on the specific element that changes, not the entire component"
Error 2: Browser-Only APIs Called During SSR
The problem: Using window, localStorage, or other browser APIs that don't exist on the server
// ❌ Crashes during server rendering
function UserPreferences() {
const theme = localStorage.getItem('theme') || 'light'
return <div className={`theme-${theme}`}>Content</div>
}
// ✅ Safe browser API usage
function UserPreferences() {
const [theme, setTheme] = useState('light')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light'
setTheme(savedTheme)
}, [])
return <div className={`theme-${theme}`}>Content</div>
}
What this does: Delays browser API calls until after hydration
Expected output: Component renders with default state, then updates with browser data
My localStorage theme picker working perfectly after the fix
Personal tip: "I create a custom hook called useClientOnly for all browser APIs - saves tons of repetition"
Error 3: Third-Party Scripts Loading at Wrong Time
The problem: Analytics, ads, or widget scripts that inject content unpredictably
// ❌ Script injects content and breaks hydration
import Script from 'next/script'
function BlogPost() {
return (
<article>
<h1>My Post</h1>
<div id="comments-widget"></div>
<Script
src="https://comments.example.com/widget.js"
strategy="afterInteractive"
/>
</article>
)
}
// ✅ Proper script loading with hydration safety
function BlogPost() {
const [scriptsLoaded, setScriptsLoaded] = useState(false)
return (
<article>
<h1>My Post</h1>
<div id="comments-widget" suppressHydrationWarning>
{!scriptsLoaded && <div>Loading comments...</div>}
</div>
<Script
src="https://comments.example.com/widget.js"
strategy="afterInteractive"
onLoad={() => setScriptsLoaded(true)}
/>
</article>
)
}
What this does: Provides predictable content until scripts finish loading
Expected output: Smooth transition from loading state to script content
Personal tip: "I always wrap third-party widget containers with suppressHydrationWarning - they're hydration error magnets"
Error 4: Conditional Rendering Based on Client State
The problem: Showing different content based on user authentication or preferences
// ❌ Server renders logged-out state, client shows logged-in
function Navigation() {
const { user } = useAuth()
if (user) {
return <AuthenticatedNav user={user} />
}
return <PublicNav />
}
// ✅ Consistent initial render with graceful loading
function Navigation() {
const { user, isLoading } = useAuth()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Always render the same thing during SSR
if (!mounted || isLoading) {
return <NavigationSkeleton />
}
return user ? <AuthenticatedNav user={user} /> : <PublicNav />
}
What this does: Shows consistent skeleton until client state is ready
Expected output: No flash of wrong content, smooth user experience
Skeleton loading prevents the auth state hydration mismatch
Personal tip: "I built a useHydrationSafe hook that handles the mounted state pattern - use it everywhere"
Error 5: Next.js 15 Strict Mode Changes
The problem: React 19's stricter hydration checks catch issues that worked in Next.js 14
// ❌ This worked in Next.js 14 but breaks in 15
function ProductCard({ product }) {
const price = product.price.toFixed(2) // Might be undefined during SSR
return (
<div>
<h3>{product.name}</h3>
<span>${price}</span>
</div>
)
}
// ✅ Defensive coding for Next.js 15
function ProductCard({ product }) {
const price = product?.price?.toFixed(2) || '0.00'
return (
<div>
<h3>{product?.name || 'Loading...'}</h3>
<span>${price}</span>
</div>
)
}
What this does: Handles undefined data gracefully during SSR/hydration
Expected output: No more "Cannot read property" errors during hydration
Personal tip: "Next.js 15 is way stricter about undefined values - add optional chaining everywhere"
My Debug Process (Copy This Exact Method)
When you hit a hydration error, follow these steps in order:
Step 1: Find the Exact Component
# Enable detailed hydration logging
export NODE_ENV=development
npm run dev
Look for this in your console:
Warning: Text content did not match. Server: "Loading..." Client: "Welcome back, John"
at div
at UserGreeting (/components/UserGreeting.tsx:12:5)
What this tells you: The exact component and line number causing issues
Step 2: Check for Browser APIs
Search your component for these hydration killers:
windowlocalStoragesessionStoragenavigatordocument.getElementById
Step 3: Add Debug Logging
function ProblematicComponent() {
console.log('Server/Client render:', typeof window !== 'undefined' ? 'CLIENT' : 'SERVER')
// Your component logic here
}
What this shows: Whether the same code path runs on server and client
Step 4: Apply the Right Fix
| Problem Type | Solution | Example |
|---|---|---|
| Dynamic content | suppressHydrationWarning | Time, user-specific data |
| Browser APIs | useEffect + useState | localStorage, window size |
| Third-party scripts | Script loading states | Analytics, widgets |
| Async data | Loading states | API calls, auth checks |
Advanced Hydration Patterns I Use
Custom Hook for Hydration Safety
// hooks/useHydrationSafe.ts
export function useHydrationSafe() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return mounted
}
// Usage in components
function SafeComponent() {
const mounted = useHydrationSafe()
if (!mounted) {
return <ComponentSkeleton />
}
return <ActualComponent />
}
Environment-Aware Rendering
function ClientOnlyComponent({ children }) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
return <>{children}</>
}
// Use it like this
<ClientOnlyComponent>
<MapWidget /> {/* Contains browser APIs */}
</ClientOnlyComponent>
What You Just Fixed
You now have working Next.js 15 components that render consistently on server and client.
Key Takeaways (Save These)
- suppressHydrationWarning: Only use on elements with intentionally different content, not entire components
- useEffect pattern: Best practice for all browser APIs - prevents SSR crashes and hydration mismatches
- Loading states: Always provide fallback content during hydration - users prefer loading indicators to broken layouts
Your Next Steps
Pick one:
- Beginner: [Set up Next.js 15 error boundaries for better debugging]
- Intermediate: [Optimize Next.js 15 streaming and concurrent features]
- Advanced: [Build custom hydration strategies for complex apps]
Tools I Actually Use
- React DevTools Profiler: Catches hydration timing issues before production
- Next.js Bundle Analyzer: Shows which components cause hydration problems
- Chrome DevTools: Application tab shows localStorage/sessionStorage issues