Skip to the solution
HardFrontend14 min read

How do you diagnose and fix hydration mismatches in Vue SSR / Nuxt 3?

68 views

The problem

Question presented to candidate: "After we moved our Nuxt app to SSR, the console shows 'Hydration completed but contains mismatches' on some pages, and a dark-mode class sometimes appears wrong. How do you find out what is mismatching, and how do you fix it properly rather than just hiding the warning?"

What a strong answer should cover:

  • What a mismatch is: the first client render does not produce the HTML the server sent, so hydration cannot simply adopt the DOM.
  • Reading the dev warning: the type (text, children, class, style, attribute, node), the server value vs the client value, and the component trace.
  • Typical causes: Date/Math.random(), window/localStorage in setup, locale or time-zone formatting, missing state transfer, and invalid HTML nesting that the browser parser rewrites.
  • What Vue does about it: text and node mismatches are patched, while class, style and attribute mismatches are check-only and stay wrong in production.
  • The fixes: move browser-only work to onMounted, useId() for IDs, transfer state (useState/useFetch in Nuxt), <ClientOnly>, and data-allow-mismatch only for genuinely expected differences.
  • Production diagnostics: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__, or debug: { hydration: true } in Nuxt.

Clarifying questions expected:

  • "Does it happen in development too, or only in production builds?"
  • "Is the differing value user-specific (theme, locale, time zone) or random?"

Code / implementation expected: Optional โ€” a component that renders a neutral value on the server and fills in a browser-only value in onMounted.

  • ssr
  • hydration
  • mismatch

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read ยท 12 sections

Read the code

VueHydration-safe patterns: onMounted for browser values, data-allow-mismatch, useId
<script setup>
import { ref, onMounted, useId } from "vue";

// 1. Browser-only value: start with something the server can render too,
//    then switch to the real value after mount (onMounted never runs on the server).
const width = ref(null);
onMounted(() => { width.value = window.innerWidth; });

// 2. A value that legitimately differs (a timestamp): render it, but tell Vue
//    the text mismatch is expected with data-allow-mismatch.
const renderedAt = new Date().toISOString().slice(11, 19);

// 3. IDs that must match on both sides: useId() is deterministic per component position.
const emailId = useId();
</script>

<template>
  <p>Viewport width: {{ width === null ? "measuring..." : width + "px" }}</p>
  <p data-allow-mismatch="text">Rendered at {{ renderedAt }}</p>
  <label :for="emailId">Email</label>
  <input :id="emailId" type="email" />
</template>
VueNuxt page with useState, onMounted and ClientOnly (built and checked on Nuxt 4.6.1)
<script setup lang="ts">
// Nuxt page: three hydration-safe patterns (ref, onMounted, useState and ClientOnly are auto-imported).

// 1. Shared state created on the server and serialised into the payload:
//    the client reuses the server value instead of computing a new one.
const greeting = useState("greeting", () => (import.meta.server ? "Hello from the server" : "never used"));

// 2. Browser-only data: render a neutral value first, fill it in after mount.
const tz = ref("");
onMounted(() => { tz.value = Intl.DateTimeFormat().resolvedOptions().timeZone; });
</script>

<template>
  <main>
    <p class="greeting">{{ greeting }}</p>
    <p class="tz">Your time zone: {{ tz || "detecting..." }}</p>
    <!-- 3. A widget that cannot render on the server at all -->
    <ClientOnly>
      <p class="widget">browser-only widget</p>
      <template #fallback><p class="widget">loading widget</p></template>
    </ClientOnly>
  </main>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 92 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
End of the topic

That was the last Vue.js question.

Pick another topic