Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/ssr-hydration-mismatch-causes.md
1---2title: Understand and Fix SSR Hydration Mismatches3impact: HIGH4impactDescription: Hydration mismatches cause visual flickering, performance loss, and broken functionality5type: gotcha6tags: [vue3, ssr, hydration, debugging, nuxt, server-side-rendering]7---89# Understand and Fix SSR Hydration Mismatches1011**Impact: HIGH** - Hydration mismatches occur when the HTML rendered on the client differs from what the server rendered. Vue attempts to recover by discarding and re-rendering mismatched nodes, causing performance degradation, visual flickering, and potentially broken event handlers.1213Understanding the common causes helps you prevent and debug these issues effectively.1415## Task Checklist1617- [ ] Validate HTML structure for proper nesting (no div in p, no nested a tags)18- [ ] Move random value generation to onMounted or use seeded randoms19- [ ] Format dates/times on client side only20- [ ] Use `data-allow-mismatch` (Vue 3.5+) for intentional mismatches21- [ ] Check for browser-modified HTML in dev tools2223## Cause 1: Invalid HTML Nesting2425Browsers auto-correct invalid HTML, creating different DOM than Vue expects.2627**Incorrect:**28```vue29<template>30<!-- WRONG: <div> cannot be inside <p> -->31<p>32<div>This will break hydration</div>33</p>3435<!-- WRONG: <a> cannot be inside <a> -->36<a href="/parent">37<a href="/child">Nested link</a>38</a>3940<!-- WRONG: Block elements in inline elements -->41<span>42<div>Block in inline</div>43</span>44</template>45```4647Browser converts the first example to:48```html49<p></p>50<div>This will break hydration</div>51<p></p>52```5354**Correct:**55```vue56<template>57<!-- CORRECT: Use appropriate nesting -->58<div>59<div>This works fine</div>60</div>6162<!-- CORRECT: Single link with event handling -->63<a href="/parent" @click="handleParentClick">64<span @click.stop="handleChildClick">Nested action</span>65</a>6667<!-- CORRECT: Block element wrapper -->68<div>69<div>Block in block</div>70</div>71</template>72```7374## Cause 2: Random Values in Render7576Server and client generate different random values.7778**Incorrect:**79```vue80<template>81<!-- WRONG: Different ID on server vs client -->82<div :id="'field-' + Math.random()">83Form field84</div>8586<!-- WRONG: Random order differs -->87<div v-for="item in shuffledItems" :key="item.id">88{{ item.name }}89</div>90</template>9192<script setup>93import { computed } from 'vue'9495const items = [/* ... */]9697// WRONG: Random shuffle runs differently on server and client98const shuffledItems = computed(() =>99[...items].sort(() => Math.random() - 0.5)100)101</script>102```103104**Correct - Client-Only Random:**105```vue106<template>107<div :id="fieldId">108Form field109</div>110111<div v-for="item in displayItems" :key="item.id">112{{ item.name }}113</div>114</template>115116<script setup>117import { ref, onMounted } from 'vue'118119const items = [/* ... */]120121// CORRECT: Start with deterministic value122const fieldId = ref('field-default')123const displayItems = ref(items) // Original order on server124125onMounted(() => {126// Randomize only on client127fieldId.value = 'field-' + Math.random().toString(36).slice(2)128displayItems.value = [...items].sort(() => Math.random() - 0.5)129})130</script>131```132133**Correct - Seeded Random:**134```javascript135// utils/seededRandom.js136export function createSeededRandom(seed) {137return function() {138seed = (seed * 9301 + 49297) % 233280139return seed / 233280140}141}142143// Use same seed on server and client144const seed = 12345 // Could be based on user ID, page, etc.145const random = createSeededRandom(seed)146```147148## Cause 3: Timezone and Date Differences149150Server may be in different timezone than client.151152**Incorrect:**153```vue154<template>155<!-- WRONG: Server time != client time -->156<span>{{ new Date().toLocaleTimeString() }}</span>157158<!-- WRONG: Server formats dates in server's timezone -->159<span>{{ formatDate(article.createdAt) }}</span>160</template>161162<script setup>163function formatDate(date) {164return new Date(date).toLocaleDateString()165}166</script>167```168169**Correct:**170```vue171<template>172<!-- CORRECT: Render placeholder, update on client -->173<span>{{ displayTime || 'Loading...' }}</span>174175<!-- CORRECT: Use UTC or defer to client -->176<span>{{ formattedDate }}</span>177</template>178179<script setup>180import { ref, computed, onMounted } from 'vue'181182const props = defineProps(['article'])183const displayTime = ref(null)184const isClient = ref(false)185186onMounted(() => {187displayTime.value = new Date().toLocaleTimeString()188isClient.value = true189})190191// CORRECT: Server renders UTC, client converts to local192const formattedDate = computed(() => {193if (!props.article?.createdAt) return ''194195if (isClient.value) {196// Client: user's local timezone197return new Date(props.article.createdAt).toLocaleDateString()198} else {199// Server: consistent UTC format200return new Date(props.article.createdAt).toISOString().split('T')[0]201}202})203</script>204```205206## Cause 4: Browser Extensions and Modifications207208Browser extensions can inject content into the DOM.209210**Mitigation:**211```vue212<template>213<!-- Use data-allow-mismatch for areas extensions might modify -->214<head data-allow-mismatch>215<title>{{ pageTitle }}</title>216</head>217</template>218```219220## Vue 3.5+ Suppressing Intentional Mismatches221222```vue223<template>224<!-- Suppress specific mismatch types -->225<div data-allow-mismatch="text">226{{ clientOnlyText }}227</div>228229<!-- Suppress all mismatches for this element -->230<div data-allow-mismatch>231<ComplexClientComponent />232</div>233</template>234```235236Valid `data-allow-mismatch` values:237- `text` - Text content mismatches238- `children` - Child element mismatches239- `class` - Class attribute mismatches240- `style` - Style attribute mismatches241- `attribute` - Other attribute mismatches242- (no value) - All mismatches243244## Debugging Hydration Mismatches245246```javascript247// Enable detailed hydration mismatch warnings in development248// vite.config.js249export default {250define: {251__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true252}253}254```255256```vue257<script setup>258import { onMounted } from 'vue'259260// Debug: Compare server HTML with client expectation261onMounted(() => {262const serverHTML = document.getElementById('app').innerHTML263console.log('Server rendered:', serverHTML)264})265</script>266```267268## Common Error Messages269270| Error | Likely Cause |271|-------|--------------|272| "Hydration text content mismatch" | Different text on server/client (dates, random) |273| "Hydration children mismatch" | Invalid HTML nesting, conditional rendering |274| "Hydration attribute mismatch" | Dynamic attributes with different values |275| "Hydration node mismatch" | Completely different elements rendered |276277## Reference278- [Vue.js SSR Guide - Hydration Mismatch](https://vuejs.org/guide/scaling-up/ssr.html#hydration-mismatch)279- [Nuxt Hydration Best Practices](https://nuxt.com/docs/guide/best-practices/hydration)280- [data-allow-mismatch RFC](https://github.com/vuejs/core/pull/9562)281