skip to content

Hydration Pitfalls

How the client adopts server HTML and what makes it mismatch: browser-only values, invalid HTML nesting, time and randomness. Interviewers probe the warnings, data-allow-mismatch and useId.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

A Vue 3 SSR component renders `new Date().toLocaleTimeString()` and hydration warns about a text mismatch; why, and how would you fix it?

level: middleimportance: must knowfreq 55%

answer

  1. the component renders twice
  2. two instants, two time zones
  3. server locale versus visitor locale
  4. defer the value past hydration
  5. onMounted plus a placeholder

basics

~20 s

The component renders once on the server and again in the browser, at a different instant and often in a different time zone and locale, so the text differs. Render a stable placeholder and set the time in onMounted, or make the value deterministic.

solid answer

~50 s

Setup and render run on the server to produce HTML, then run again in the browser during hydration. `new Date()` is read at two different moments, and `toLocaleTimeString()` formats with the Node process's default time zone and locale on one side and the visitor's on the other, so the strings differ. Vue reports a text mismatch in development and overwrites the text with the client value, so the page works but the warning is real. The usual fix is to keep the value out of the hydrating render: hold it in a `ref` that starts as a placeholder and set it in `onMounted`, which never runs on the server and runs on the client only after hydration. If the time itself must be server-rendered, capture it once on the server, ship it to the client, and format it with an explicit locale and `timeZone`. `data-allow-mismatch="text"` (3.5+) is the fallback for a difference you accept.

code

vue · 20 lines
vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'

const time = ref<string | null>(null)
let timer: ReturnType<typeof setInterval> | undefined

onMounted(() => {
  const tick = () => {
    time.value = new Date().toLocaleTimeString()
  }
  tick()
  timer = setInterval(tick, 1000)
})

onUnmounted(() => clearInterval(timer))
</script>

<template>
  <span class="clock">{{ time ?? '--:--:--' }}</span>
</template>

go deeper

for a junior

Recall that the component renders on the server and again in the browser, and that time differs between those runs. Know the onMounted placeholder fix.

for a middle

Separate the two causes, a moving clock and a different zone and locale, and explain why onMounted works: it never runs on the server and runs after hydration.

for a senior

Choose between client-only, deterministic formatting and suppression per case, and point out that ignored warnings drown out real mismatches in development.

for a principal

Treat user-local presentation (time, currency, locale) as a policy: decide what the server can know about the visitor and where formatting happens, so teams stop solving it component by component.

## Why the text differs In a server-rendered Vue 3 app, each component's `setup` and render run **twice**: 1. On the server, to produce the HTML string the browser receives. 2. In the browser, during **hydration**, where Vue renders the same tree again and walks the existing DOM to adopt it. Anything that is not a pure function of the component's inputs can come out differently between those two runs. A clock is the textbook case, and it differs for two independent reasons: - **Time moves.** The server read `new Date()` when it rendered; the browser reads it again after downloading and executing the bundle, seconds later. - **Time zone and locale differ.** `toLocaleTimeString()` with no arguments uses the runtime's default time zone and locale. On the server that is the Node process's environment (often UTC and an English locale); in the browser it is the visitor's settings. Even the same instant can print as `14:05:09` on one side and `4:05:09 PM` on the other. The same trap applies to `Date.now()`, relative labels such as "5 minutes ago", and `Intl` formatting without explicit options. ## What Vue does about it When the text of an element differs, Vue 3.5: - in **development**, warns about a text mismatch and prints the server and client values; - **overwrites** the DOM text with the client value, so what the user finally sees is the browser's time; - in **production**, prints no detail, only one generic `Hydration completed but contains mismatches.` error per page load. So the page does not break, but the warning is not noise: the server's work for that node was wasted, the text may visibly jump, and a flood of such warnings hides real mismatches. ## Fix options | Option | How | When it fits | |---|---|---| | **Client-only value** | `ref` starts as a placeholder, `onMounted` sets the real time | live clocks, user-local times, anything that must reflect the browser | | **Deterministic value** | capture the instant once on the server, pass it to the client, format with an explicit locale and `timeZone` | the time must be in the HTML (for example, an article's publish time) | | **Accept the mismatch** | `data-allow-mismatch="text"` on the element that wraps the value (Vue 3.5+) | a harmless, inherent difference you prefer to show with client formatting | ### The client-only pattern The Vue SSR guide recommends rendering values like this only on the client, using `onMounted`. Two lifecycle facts make it work: - `onMounted` **never runs during server rendering**, so the server HTML contains the placeholder. - On the client it runs **after hydration** of that component has finished, so the hydrating render also produces the placeholder, and the DOM matches. The update to the real time is then an ordinary reactive update, not a hydration difference. Keep the placeholder's markup shape identical on both sides; a placeholder with the same element and a fixed width also avoids layout shift. ### The deterministic pattern A publish time like `2026-09-25T14:05:09Z` is data, not "now". Format it with an explicit locale and zone, for example `new Intl.DateTimeFormat('en-GB', { timeZone: 'UTC', timeStyle: 'short' })`, and both runtimes print the same string. If you want the visitor's local zone, that is by definition unknown to the server, so fall back to the client-only pattern. ### The suppression attribute `data-allow-mismatch="text"` tells Vue that a text difference on that element is expected, so it stops reporting it. It does **not** make Vue keep the server text: the client value still replaces it. Put it on the element that directly wraps the value. ## Things that do not fix it - Wrapping the value in `computed`: a computed is evaluated on both sides, just like inline template code. - Reading the time in `onBeforeMount`: it also runs before the hydrating render on the client and never on the server, so the server and client renders still disagree. - Guarding with `typeof window !== 'undefined'` inside the render: that is the textbook way to make server and client output differ on purpose.

  • Why does moving the `new Date()` call into `computed` not help?
    A `computed` is evaluated whenever the render reads it, and the render runs on the server and again during hydration. It is still read at two different moments in two different environments, so the text still differs. Only code that runs after hydration, such as `onMounted`, keeps the value out of the hydrating render.
  • What does the user see if you ignore the warning?
    Vue replaces the text node's content with the client value during hydration, so the final page shows the browser's time. The server's text may be visible briefly before it jumps, production logs a generic mismatch error on every page load, and real mismatches become harder to spot among the expected ones.
  • When is `data-allow-mismatch="text"` the right choice over `onMounted`?
    When the value is useful in the server HTML and the client difference is inherent and harmless, for example a date that is pre-rendered but reformatted in the visitor's locale. It silences the report while Vue still applies the client text. For a live clock, `onMounted` is cleaner because there is nothing meaningful to server-render.

saying these in an interview costs you the question

  • Vue keeps the server text, so the user sees a stale time
  • Wrapping the date in computed makes it the same on both sides
  • onMounted runs on the server too, just earlier
  • The server and browser always share a time zone
  • A text mismatch makes the component non-interactive
open as a page

In a Vue 3 SSR app, why does a template containing `<p><div>Hi</div></p>` cause a hydration mismatch even though the server rendered exactly that?

level: juniorimportance: should knowfreq 42%

basics

~20 s

The browser's HTML parser, not Vue, turns the server string into DOM, and it closes the paragraph before the div. The resulting DOM no longer matches the vnode tree the client renders, so Vue reports a mismatch and rebuilds those nodes.

open as a page

In Vue 3.5, what does the data-allow-mismatch attribute do, which values does it accept, and when is it the wrong fix?

level: middleimportance: should knowfreq 35%

basics

~20 s

data-allow-mismatch (Vue 3.5+) marks a hydration mismatch as expected, so Vue stops reporting it; it does not change how Vue recovers. Values are text, children, class, style or attribute, comma-separated, or empty for all. It is wrong for bugs you can fix.

open as a page

In a Vue 3.5 SSR app, why should a form component generate its label and input ids with useId() instead of Math.random() or a counter?

level: middleimportance: should knowfreq 40%

basics

~20 s

Random values and module counters produce different ids on the server and in the browser, so id and for attributes mismatch during hydration. useId, added in Vue 3.5, derives the id from the component's place in the app tree, so both renders agree.

open as a page

A Vue 3.5 SSR app logs only 'Hydration completed but contains mismatches.' in production, and a theme class is stuck wrong after load; how do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Production builds print one generic error, so reproduce with a development SSR build or a production build with VUE_PROD_HYDRATION_MISMATCH_DETAILS enabled. The stuck class is likely a separate, silent mismatch: class differences are not corrected, and plain production builds do not even check them.

open as a page