skip to content

In Vue 3 SSR, what does onServerPrefetch do, and why must the data it fetches also reach the client?

level: middleimportance: should knowfreq 45%

answer

  1. SSR-only lifecycle hook
  2. renderer awaits the promise
  3. client never calls it
  4. serialize state into the page

basics

~20 s

onServerPrefetch registers an async callback that Vue's server renderer awaits before rendering that component, so fetched data lands in the HTML. The client never calls it, so the data must be serialized and restored, or the client starts empty and refetches.

solid answer

~40 s

`onServerPrefetch(callback)` is an SSR-only lifecycle hook (the Options API form is `serverPrefetch`). The server renderer runs setup, collects every prefetch callback the component registered, awaits them, and only then renders the component, so data assigned to reactive state inside the callback appears in the HTML. Children render after their parent's prefetch resolves, so nested prefetches form a waterfall. If the promise rejects, the error goes through Vue's error handling (`onErrorCaptured`, `app.config.errorHandler`) and the component still renders with whatever state it has. On the client the hook is never called and setup starts again from its initial state, so the fetched data has to live in per-request state that the server serializes into the page and the client restores before mounting; otherwise the client renders empty, mismatches the HTML and fetches again.

code

ts · 13 lines
ts
import { createSSRApp, reactive, type InjectionKey } from 'vue'
import App from './App.vue'

export interface ProductsState { products: { id: number; name: string }[] | null }
export const productsKey: InjectionKey<ProductsState> = Symbol('products')

// called once per request on the server, once on page load in the browser
export function createApp(initial?: ProductsState) {
  const app = createSSRApp(App)
  const state = reactive<ProductsState>(initial ?? { products: null })
  app.provide(productsKey, state)
  return { app, state } // the server serializes `state`; the client passes it back as `initial`
}

go deeper

for a junior

Know that onServerPrefetch is the SSR-only hook for loading data into the HTML, and onMounted is its client-side counterpart.

for a middle

Explain the order: setup, awaited prefetch, then render; and why the client needs the fetched state serialized into the page to avoid a refetch and mismatch.

for a senior

Diagnose nested prefetch waterfalls and error paths, lift fetches where needed, and design the per-request state that the server serializes and the client restores.

for a principal

Judge whether hand-wiring prefetch plus state transfer is worth owning, or whether a framework's data layer should take over caching, deduplication and serialization.

## What onServerPrefetch is `onServerPrefetch` is a Composition API lifecycle hook marked **SSR only** in the Vue API reference. Its type is `onServerPrefetch(callback: () => Promise<any>): void`: you register an async function, and if it returns a Promise, the server renderer waits for it before rendering that component. The Options API equivalent is the `serverPrefetch` option. It exists because the usual client place for loading data, `onMounted`, is never called during SSR. `onServerPrefetch` is the hook that lets a component load data on the server so the result is part of the HTML. ## How the server renderer uses it For every component, Vue's server renderer: 1. creates the instance and runs **setup**, which registers the hook; 2. collects all the prefetch callbacks that component registered and awaits them together; 3. renders the component's subtree, reading whatever state the callbacks filled; 4. repeats the process for each child it meets while rendering that subtree. Consequences worth saying in an interview: - **Waterfalls**: a child's setup, and so its prefetch, starts only after the parent's prefetch resolved. Deeply nested fetches add their latencies together. - **Siblings overlap**: sibling components are started as the parent renders, so their prefetches run concurrently. - **Errors do not abort the page**: the hook is wrapped in Vue's async error handling, so a rejection is reported to `onErrorCaptured` hooks and `app.config.errorHandler`, and the component then renders with whatever state it has. Render a fallback for the empty case. ## The client never runs it | Code | Server render | Browser (hydration) | |---|---|---| | setup / root of `<script setup>` | runs | runs again, from initial state | | `onServerPrefetch` callback | runs and is awaited | never called | | `onMounted` callback | never called | runs after hydration | If the data only lives in a component-local `ref`, the browser's copy of that ref starts at its initial value. The client's first render then differs from the server HTML, Vue has to repair the DOM, and an `onMounted` fallback fetches the same data a second time. ## Transferring state to the client The standard shape: 1. Keep fetched data in **per-request state** created by the app factory and provided to components, not only in a component-local ref. 2. After rendering, the server handler **serializes** that state into the page, escaping it so user content cannot break out of the script tag. 3. The client entry **restores** the state into its store before calling `mount`, so hydration renders the same data. 4. The component keeps an `onMounted` fallback that fetches only when the data is missing, which is the case when it is first rendered by client-side navigation rather than by the server. ```vue <script setup lang="ts"> import { inject, onMounted, onServerPrefetch } from 'vue' import { productsKey, fetchProducts } from './state' const state = inject(productsKey)! // per-request state the server serializes onServerPrefetch(async () => { state.products = await fetchProducts() }) onMounted(async () => { if (!state.products) state.products = await fetchProducts() }) </script> ``` ## Where data can be loaded | Place | Runs on the server | Runs in the browser | Awaited before the component renders | |---|---|---|---| | Root of `<script setup>` | yes | yes | no: a promise started there is not awaited by the renderer | | `onServerPrefetch` | yes | no | yes | | `onMounted` | no | yes | no: it runs after the component is mounted | Only `onServerPrefetch` combines running on the server with being awaited, which is why it is the hook for data that must be in the HTML. ## What it is not - It is not a data layer: it does no caching, deduplication or retry, so two components prefetching the same resource fetch it twice. - It is not a place for browser APIs: it runs only on the server. - It does not delay the client: hydration never waits for it, because it never runs there. Frameworks built on Vue wrap this primitive together with state transfer into higher-level data-fetching helpers; with Vue core alone you wire the serialization yourself.

  • What happens in Vue 3 SSR when the promise returned from an onServerPrefetch callback rejects?
    The hook runs through Vue's async error handling, so the error is passed to `onErrorCaptured` hooks up the tree and then to `app.config.errorHandler`, or logged if none handles it. The server renderer then carries on and renders the component with whatever state it has. The component should therefore render a sensible empty or error state rather than assume the data arrived.
  • Three nested Vue components each fetch in onServerPrefetch and the page is slow. What is happening, and what can you do?
    A child's setup, and so its prefetch, only starts after its parent's prefetch resolved and the parent began rendering, so the three requests run one after another. Lift the fetches to the top component and start them together, or pass data down so children need not fetch. Siblings already overlap; only nesting serialises.

saying these in an interview costs you the question

  • onServerPrefetch also runs in the browser just before hydration.
  • A rejected onServerPrefetch promise always aborts the whole server render.
  • Data set in onServerPrefetch automatically appears in the client's refs.
  • onMounted is the right place to fetch data that must be in the server HTML.
  • Nested components' onServerPrefetch callbacks all start in parallel.