A Vue 3 SSR component renders `new Date().toLocaleTimeString()` and hydration warns about a text mismatch; why, and how would you fix it?
answer
- the component renders twice
- two instants, two time zones
- server locale versus visitor locale
- defer the value past hydration
- onMounted plus a placeholder
basics
~20 sThe 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 sSetup 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<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
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.
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.
Choose between client-only, deterministic formatting and suppression per case, and point out that ignored warnings drown out real mismatches in development.
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