What Causes This Error?
Hydration mismatches occur when the HTML rendered on the server differs from what Vue expects to render on the client. During hydration, Vue tries to attach event listeners and reactivity to existing server-rendered DOM, but if the structure differs, it causes problems.
The Problem
<script setup>
// ❌ Different output on server vs client
const currentTime = new Date().toLocaleTimeString()
</script>
<template>
<p>Time: {{ currentTime }}</p> <!-- Different on server/client! -->
</template>
The Fix
Use Client-Only Rendering
<script setup>
import { ref, onMounted } from 'vue'
const currentTime = ref('')
// ✅ Only set on client
onMounted(() => {
currentTime.value = new Date().toLocaleTimeString()
})
</script>
<template>
<p>Time: {{ currentTime || 'Loading...' }}</p>
</template>
Use ClientOnly Component (Nuxt)
<template>
<!-- ✅ Only renders on client -->
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<p>Loading...</p>
</template>
</ClientOnly>
</template>
Check for Browser APIs
<script setup>
import { ref, onMounted } from 'vue'
const windowWidth = ref(0)
// ❌ window doesn't exist on server
// const width = window.innerWidth
// ✅ Check for client environment
onMounted(() => {
windowWidth.value = window.innerWidth
})
</script>
Common Causes
1. Date/Time Values
// ❌ Different times on server vs client
const timestamp = Date.now()
// ✅ Use consistent server timestamp or client-only
const timestamp = ref(null)
onMounted(() => {
timestamp.value = Date.now()
})
2. Random Values
// ❌ Different random values
const id = Math.random().toString(36)
// ✅ Generate on server and pass down, or client-only
const id = ref('')
onMounted(() => {
id.value = Math.random().toString(36)
})
3. Browser-Only Data
// ❌ localStorage doesn't exist on server
const theme = localStorage.getItem('theme')
// ✅ Check environment
const theme = ref('light')
onMounted(() => {
theme.value = localStorage.getItem('theme') || 'light'
})
4. Conditional Rendering Based on Client State
<!-- ❌ Different on server vs client -->
<template>
<div v-if="isMobile">Mobile View</div>
<div v-else>Desktop View</div>
</template>
<!-- ✅ Use CSS or client-only detection -->
<template>
<div class="mobile-only">Mobile View</div>
<div class="desktop-only">Desktop View</div>
</template>
<style>
@media (max-width: 768px) {
.desktop-only { display: none; }
}
@media (min-width: 769px) {
.mobile-only { display: none; }
}
</style>
5. Third-Party Libraries
<script setup>
import { onMounted, ref } from 'vue'
const chartRef = ref(null)
// ❌ Chart library modifies DOM differently
// import Chart from 'chart.js'
// ✅ Dynamic import on client
onMounted(async () => {
const { Chart } = await import('chart.js')
new Chart(chartRef.value, { /* config */ })
})
</script>
Debugging Hydration Mismatches
// Enable detailed hydration mismatch warnings
const app = createSSRApp(App)
app.config.warnHandler = (msg, instance, trace) => {
if (msg.includes('Hydration')) {
console.warn('Hydration mismatch:', msg)
console.log('Component:', instance)
console.log('Trace:', trace)
}
}
Vue 3.4+ Hydration Mismatch Details
// Vue 3.4+ provides more details
app.config.performance = true
// In browser console, you'll see:
// [Vue warn]: Hydration node mismatch:
// - rendered on server: <div class="foo">
// - expected on client: <div class="bar">
Suppressing Specific Mismatches
<!-- ✅ Tell Vue to skip hydration for this element -->
<div v-html="dynamicContent" data-allow-mismatch></div>
Quick Checklist
- Avoid
Date.now(),Math.random()during SSR - Use
onMountedfor browser-only code - Check for
window,document,localStorageexistence - Use
<ClientOnly>wrapper for browser-specific components - Ensure same data is used on server and client
- Dynamic imports for browser-only libraries
- Use CSS media queries instead of JS-based responsive logic