What Causes This Error?
This error occurs when the HTML rendered on the server doesn’t match the HTML generated on the client during hydration. Nuxt uses Vue’s hydration process to take the server-rendered HTML and make it interactive, but any mismatch causes Vue to abandon the hydration and re-render from scratch.
The Problem
<script setup>
import { ref } from 'vue'
const counter = ref(0)
// ❌ Problem: Date/time changes between server and client
const now = ref(new Date().toLocaleTimeString())
</script>
<template>
<div>
<!-- Will cause hydration mismatch -->
<p>Current time: {{ now }}</p>
</div>
</template>
Common Causes and Fixes
Cause 1: Client-Only Data
Data that differs between server and client:
<script setup>
const isClient = ref(process.client)
// ❌ Wrong: Will always be false on server, true on client
</script>
<template>
<div>{{ isClient ? 'Client' : 'Server' }}</div>
</template>
Fix: Use <ClientOnly> component:
<script setup>
import { ref } from 'vue'
const isClient = ref(process.client)
</script>
<template>
<!-- ✅ Only rendered on client -->
<ClientOnly>
<div>{{ isClient ? 'Client' : 'Server' }}</div>
</ClientOnly>
</template>
Cause 2: Random Values
<script setup>
const randomId = ref(Math.random())
</script>
<template>
<div :id="randomId">Content</div>
</template>
Fix: Generate random IDs only on client:
<script setup>
import { ref, onMounted } from 'vue'
const randomId = ref('')
onMounted(() => {
randomId.value = Math.random().toString(36).substr(2, 9)
})
</script>
<template>
<div :id="randomId">Content</div>
</template>
Cause 3: Browser APIs
<script setup>
// ❌ window is undefined on server
const width = ref(window.innerWidth)
</script>
<template>
<div>Width: {{ width }}</div>
</template>
Fix: Use onMounted:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const width = ref(0)
const updateWidth = () => {
width.value = window.innerWidth
}
onMounted(() => {
if (process.client) {
updateWidth()
window.addEventListener('resize', updateWidth)
}
})
onUnmounted(() => {
if (process.client) {
window.removeEventListener('resize', updateWidth)
}
})
</script>
<template>
<div>Width: {{ width }}</div>
</template>
Cause 4: Conditional Rendering Based on Process
<script setup>
const isClient = process.client
</script>
<template>
<!-- ❌ Different DOM structure on server vs client -->
<div v-if="isClient">Client content</div>
<div v-else>Server content</div>
</template>
Fix: Use <ClientOnly> or defer rendering:
<template>
<!-- ✅ Option 1: ClientOnly -->
<ClientOnly>
<div>Client content</div>
<template #fallback>
<div>Server content</div>
</template>
</ClientOnly>
<!-- ✅ Option 2: State management -->
<div v-if="mounted">Client content</div>
<div v-else>Server content</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>
Cause 5: Third-Party Components
Some components have internal state that causes mismatches:
<script setup>
import SomeLibraryComponent from 'some-library'
</script>
<template>
<!-- ❌ Component may render differently on server vs client -->
<SomeLibraryComponent />
</template>
Fix: Wrap in <ClientOnly>:
<template>
<ClientOnly>
<SomeLibraryComponent />
<template #fallback>
<div>Loading component...</div>
</template>
</ClientOnly>
</template>
Cause 6: Cookies and LocalStorage
<script setup>
// ❌ Cookies differ between server and client
const theme = ref(useCookie('theme').value || 'light')
</script>
<template>
<div :class="theme">Content</div>
</template>
Fix: Use consistent defaults:
<script setup>
const theme = ref('light')
// Update on client mount
onMounted(() => {
const cookie = useCookie('theme')
if (cookie.value) {
theme.value = cookie.value
}
})
</script>
<template>
<div :class="theme">Content</div>
</template>
Cause 7: HTML Attributes with Special Values
<template>
<!-- ❌ Value differs -->
<div :data-timestamp="Date.now()">Content</div>
</template>
Fix: Use static values or client-only rendering:
<template>
<ClientOnly>
<div :data-timestamp="Date.now()">Content</div>
</ClientOnly>
</template>
Debugging Hydration Errors
Enable Vue DevTools
npm install @nuxtjs/devtools --save-dev
Use Browser Console
Open browser DevTools and check for detailed hydration warnings:
[Vue warn]: Hydration node mismatch:
- Client vnode: div
- Server rendered DOM: div (at the same position)
Check Network Tab
- Open DevTools → Network
- Reload page
- Compare initial HTML response with DOM after hydration
Advanced Solutions
Use ClientOnly with Placeholders
<template>
<ClientOnly>
<HeavyComponent />
<template #fallback>
<div class="skeleton">Loading...</div>
</template>
</ClientOnly>
</template>
Disable SSR for Specific Pages
// pages/some-page.vue
<script setup>
definePageMeta({
ssr: false
})
</script>
Use process.client Guard
<script setup>
import { computed } from 'vue'
const value = computed(() => {
if (process.client) {
return localStorage.getItem('key')
}
return null
})
</script>
<template>
<div>{{ value || 'Not available' }}</div>
</template>
Testing for Hydration Issues
// tests/e2e/hydration.spec.ts
import { test, expect } from '@playwright/test'
test('page hydrates correctly', async ({ page }) => {
await page.goto('/')
// Check no hydration errors in console
const errors: string[] = []
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text())
}
})
await page.waitForLoadState('networkidle')
expect(errors.filter(e => e.includes('hydration'))).toHaveLength(0)
})
Best Practices
- Always initialize state with the same value on server and client
- Use
<ClientOnly>for browser-specific code - Defer browser API calls to
onMounted - Avoid conditional rendering that changes DOM structure
- Test with SSR disabled (
ssr: false) to isolate client-side issues - Check third-party components for SSR compatibility
- Use environment variables carefully
Quick Checklist
- No
window/documentaccess outsideonMounted - No
Math.random()orDate.now()in initial render - Browser-specific components wrapped in
<ClientOnly> - Consistent initial state between server and client
- No conditional rendering that changes DOM structure
- Third-party libraries checked for SSR compatibility
- Cookies/localStorage accessed correctly
Related Errors
Hydration Text Content Mismatch
[Vue warn]: Hydration text content mismatch
Same issue—content differs between server and client. Follow the same fixes.
Hydration Attribute Mismatch
[Vue warn]: Hydration attribute mismatch
Attributes differ—check for dynamic attributes with client-side values.
Performance Impact
Hydration mismatches cause:
- Full client-side re-render
- Lost SEO benefits
- Poor user experience (flash of content)
- Slower Time to Interactive
Fixing these errors improves both performance and user experience.