During Vue 3 server rendering, what happens to reactivity, and how do watch, watchEffect and computed behave inside a component's setup?
answer
- one render per component
- no render effect on server
- watchers mostly stopped
- computed evaluates when read
basics
~20 sVue renders each component once on the server with no render effect, so state changes never re-render anything. In setup, lazy and post-flush watchers never run, watchEffect and immediate watchers run once then stop, and computed evaluates when read.
solid answer
~40 sThe Vue 3 SSR guide says reactivity is disabled during SSR by default: a request maps to one state, there is no user interaction and no DOM to patch. Concretely, the server renderer runs setup, awaits any `onServerPrefetch`, and calls the component's render once, with no render effect subscribed to the state. `ref` and `reactive` still work as containers, so a value set before the render appears in the HTML, but a change after that component rendered has no effect on its output. In setup, a lazy `watch` never fires, a `watchPostEffect` never runs, `watchEffect` and `watch` with `immediate: true` run once and are stopped, and `flush: 'sync'` watchers stay active only until the render finishes. `computed` still works on read, re-evaluating instead of relying on dependency tracking.
code
ts · 13 linesimport { ref, computed, watch, watchEffect, watchPostEffect } from 'vue'
// inside setup(), while the component renders on the server
const count = ref(1)
watch(count, (v) => console.log('lazy', v)) // never runs on the server
watch(count, (v) => console.log('immediate', v), { immediate: true }) // runs once, then stopped
watchEffect(() => console.log('effect', count.value)) // runs once, then stopped
watchPostEffect(() => console.log('post')) // never runs on the server
const double = computed(() => count.value * 2) // evaluated when the template reads it
count.value++ // the stopped watchers stay silent; the render will read 2 and double 4go deeper
Remember that each component is rendered once on the server: state changes after it rendered never change its HTML.
Explain the watcher table: lazy and post-flush watchers never run, immediate watchers and default watchEffect run once and stop, sync watchers live until the render ends.
Spot designs that depend on server re-rendering, such as children writing layout state or watchers deriving data, and move that work before the render or out to the SSR context.
Weigh how much of the app's state derivation is SSR-sensitive, and set conventions such as computed-only derivations so universal code behaves the same on both sides.
## Why Vue turns reactivity off on the server On the client, **reactivity** means: a component's render runs inside a **render effect**, every reactive read during that render is tracked, and a later write schedules a re-render. On the server none of that pays off. Each request URL maps to one desired state, there is no user interaction, and the output is a string rather than a DOM that could be patched. The Vue SSR guide therefore states that reactivity is disabled during SSR by default, for performance. Concretely, the server renderer does this for each component: 1. Create the instance and run **setup**. 2. Await the callbacks registered with `onServerPrefetch`, if any. 3. Call the component's server render function **once** and push the resulting strings into the output buffer. There is no render effect, so nothing subscribes to the state the render read. ## What still works - `ref()` and `reactive()` still return refs and proxies, and reads and writes behave normally. - A value written before the component renders, in setup or in `onServerPrefetch`, is what the HTML shows. That is why prefetching data works at all. - What is missing is the **subscription**. Writing to state after a component has been rendered never changes the HTML already emitted for it. That last point produces a classic bug: a deeply nested page component writes a title or a flag into shared state during its setup, expecting the layout that already rendered above it to pick it up. On the client that works through re-rendering; on the server the layout's markup is already in the buffer. Anything that must affect earlier markup has to be decided before that markup renders, or be collected on the side and injected by the server handler afterwards. ## Watchers during server setup Vue's `watch` implementation special-cases server setup: | API called in setup | Behaviour during SSR | |---|---| | `watch(source, cb)` (lazy) | callback never runs; a no-op handle is returned | | `watch(source, cb, { immediate: true })` | callback runs once, then the watcher is stopped | | `watchEffect(fn)` (default `flush: 'pre'`) | runs once, then stopped | | `watchPostEffect(fn)` or `flush: 'post'` | never runs | | any watcher with `flush: 'sync'` | stays active until the render finishes, then stopped | The reason for stopping them is housekeeping: a watcher is normally stopped when its component unmounts, and on the server nothing unmounts. A live watcher per request would be a leak. Sync watchers are the one exception because they react synchronously during the render; the server renderer collects their handles and stops them once the whole app has been rendered. ## computed on the server `computed` still returns the right value, and its getter still runs only when read. The difference is internal: on the client a computed knows it is stale because its dependencies notify it. On the server there is no render effect subscribing, so dependency tracking cannot be relied on. Vue's reactivity core marks computeds created during server setup and re-evaluates them on read, using a global version counter as a shortcut: if no reactive state changed anywhere since the last read, the cached value is returned. ## Symptoms and what they mean - A value derived by a watcher is missing from the server HTML but appears after hydration: the watcher was lazy or post-flush, so it never ran on the server. - A log line inside `watchEffect` prints once per request on the server and again in the browser: expected, because the first run happens on both sides. - The server HTML shows stale layout data that a child set: the layout rendered first and nothing re-renders it. - Watchers created in setup do not accumulate on the server, because Vue stops them; timers or subscriptions you start yourself are not stopped. ## Practical rules 1. Produce every value the markup needs before the component renders: in setup, or in `onServerPrefetch` for async data. 2. Derive values with `computed` or plain functions, not with a watcher that writes to another ref; a lazy watcher never fires on the server. 3. Keep side-effect watchers for the client. They run normally in the browser once the component is mounted there. 4. Never expect a child's change to shared state to rewrite a parent's or sibling's already-rendered HTML.
- A nested Vue component sets a page title in shared reactive state during setup, but the server HTML still shows the old title in the layout. Why?The layout rendered before the nested component's setup ran, and on the server there is no render effect to re-render the layout when the state changes. Its markup is already in the buffer. Decide the title before the layout renders, or record it on the side, for example in the SSR context from `useSSRContext()`, and have the server handler write it into the page shell after rendering.
- If watchers are stopped on the server, will a `watchEffect` in the same Vue component work in the browser?Yes. The client runs setup again from scratch, and there the watcher is created normally, runs its first time, and keeps tracking until the component unmounts. Stopping on the server only affects the server's copy of the instance, which is discarded after the HTML is produced.
saying these in an interview costs you the question
- Vue re-renders a server component when its state changes before the response ends.
- watch callbacks fire on the server whenever their source changes during rendering.
- ref() and reactive() return plain non-reactive values when running on the server.
- watchPostEffect runs once on the server after the component's HTML is emitted.
- computed cannot be used on the server because it needs dependency tracking.