skip to content

When rendering a Vue 3 app on the server, what is the SSR context object, and how do useSSRContext and ctx.teleports rely on it?

level: seniorimportance: should knowfreq 30%

answer

  1. second argument to the renderer
  2. provided to the whole tree
  3. components write, handler reads
  4. teleports collected per target

basics

~20 s

The SSR context is the object passed as the second argument to renderToString or a stream renderer. Components read it with useSSRContext() to record data for the handler, and Vue fills ctx.teleports with teleported HTML keyed by target.

solid answer

~40 s

In Vue 3, `renderToString(app, ctx)` and the stream renderers accept a plain object, the **SSR context**, which Vue provides to the rendered app. Inside a component, `useSSRContext()` from `vue` returns that same object, so components can record things the page shell needs, such as head tags or a not-found flag, for the handler to read after rendering. On the client there is no context: it returns `undefined` and warns in development, so call it only in server-only branches. Vue also writes into it: content rendered by a `<Teleport>` is not part of the returned HTML (only marker comments are), and is collected into `ctx.teleports`, keyed by the target selector. The handler must place each entry inside the matching container in the page, preferably a dedicated element rather than `body`.

code

ts · 15 lines
ts
import { renderToString } from 'vue/server-renderer'
import type { SSRContext } from 'vue/server-renderer'
import type { App } from 'vue'

export async function renderPage(app: App) {
  const ctx: SSRContext = {} // fresh per request
  const appHtml = await renderToString(app, ctx)

  const status = ctx.notFound ? 404 : 200 // set by a component via useSSRContext()
  const page = `<!DOCTYPE html><html><head><title>${ctx.title ?? 'Shop'}</title></head><body>` +
    `<div id="app">${appHtml}</div>` +
    `<div id="modals">${ctx.teleports?.['#modals'] ?? ''}</div>` +
    `</body></html>`
  return { status, page }
}

go deeper

for a junior

Know that renderToString takes an optional context object and that teleported content ends up in ctx.teleports rather than the returned HTML.

for a middle

Explain useSSRContext as a per-render channel from components to the handler, and why it must only be called on the server.

for a senior

Handle the timing: context data and teleports are complete only after the app renders, which shapes shell layout and rules out late head tags when streaming.

for a principal

Define what components may record in the SSR context, such as head, status and preloads, so every page communicates with the server shell the same way.

## What the SSR context is Every render function in `vue/server-renderer` accepts a **context** object: `renderToString(input, context?)`, `renderToNodeStream(input, context?)`, `pipeToNodeWritable(input, context, writable)` and the web-stream variants. Its type is `SSRContext`: an open object with an optional `teleports` field, plus internal fields Vue uses for bookkeeping. When rendering starts, the server renderer **provides** the context to the app under an internal injection key. The context therefore works as a per-render channel between components deep in the tree and the request handler that called the renderer. Create a new object for every render. Reusing one across requests mixes their teleports and whatever components wrote into it. ## Components writing into it: useSSRContext `useSSRContext()` is exported from `vue` and returns the context object during a server render: ```vue <script setup lang="ts"> import { useSSRContext } from 'vue' if (typeof window === 'undefined') { const ctx = useSSRContext<{ title?: string }>()! ctx.title = 'Product 42' } </script> ``` Typical uses: - head metadata such as the title or meta tags, which the handler writes into `<head>`; - a flag like `notFound`, which the handler turns into a 404 status; - a list of resources the page needs, for preload hints. The API reference says to call it only during SSR. In the browser nothing provides the context, so it returns `undefined` and logs `Server rendering context not provided. Make sure to only call useSSRContext() conditionally in the server build.` in development. In Vue's global (script-tag) build it is not supported at all. ## Vue writing into it: ctx.teleports A `<Teleport to="#modals">` renders its content somewhere else in the document. On the server that place is outside the app's own markup, so Vue cannot emit it inline: 1. In the returned HTML, the Teleport leaves only marker comments where it sits in the tree. 2. Its content is rendered into a separate buffer per target selector. 3. After the app's HTML is complete, Vue resolves those buffers into `ctx.teleports`, an object such as `{ '#modals': '<div class="modal">...</div>' }`. 4. The handler inserts each string into the matching container in the page shell. The content also carries anchor comments that let hydration find it again in the browser. Two practical rules from the SSR guide: - Prefer a dedicated container such as `<div id="modals"></div>` over `to="body"`: `<body>` also holds other server-rendered content, so hydration cannot tell where the teleported part starts. - If you do not need the content in the initial HTML, rendering the Teleport only after mount avoids the problem entirely. ## Hygiene - Values components write into the context usually end up in the page. Escape them when the handler inserts them: Vue escapes template interpolations, not the strings you concatenate into your shell. - Keep the context per request and never store it in module scope. - Guard `useSSRContext()` so client bundles do not warn: a `typeof window` check, or a build-time SSR flag your bundler provides. - Type both ends: `useSSRContext<T>()` takes a type parameter, and the `SSRContext` type is exported from `vue/server-renderer` for the handler. ## Timing with renderToString versus streams | | `renderToString` | Stream renderers | |---|---|---| | When `ctx.teleports` and component-written fields are complete | when the promise resolves | only after the app's HTML has been pushed | | Where you can put that data | anywhere in the shell, head included | only after the app root | With streams, anything recorded in the context arrives too late for bytes already sent: head tags written by components cannot reach a `<head>` that has already been flushed, and teleport containers must come after the app root. `pipeToNodeWritable` also ends the writable when rendering completes, so to append the teleports and the closing tags use `renderToSimpleStream` with your own `push`, which sees `push(null)` after the teleports have been resolved.

  • With Vue's stream renderers, why can't a component deep in the tree set the page's title through the SSR context?
    The stream sends HTML as it is produced, and the handler must write `<head>` before the app markup starts streaming. By the time a deep component writes `ctx.title`, the head is already on the wire. Either decide head data before rendering starts, or use `renderToString` for pages whose head depends on what the tree renders.
  • Why does the Vue SSR guide advise against teleporting to body when the page is server-rendered?
    The server hands you teleport HTML to insert yourself, and on the client hydration must find where that content starts inside the target. `<body>` also contains the app root and other server-rendered markup, so there is no reliable starting point. A dedicated container that holds only teleported content avoids the ambiguity.

saying these in an interview costs you the question

  • Teleported content is rendered inline in the string returned by renderToString.
  • useSSRContext() returns the server's context object in the browser too.
  • One shared context object can be reused for every request.
  • ctx.teleports is available while a stream is still sending the app's HTML.
  • Teleporting to body is the recommended target for server-rendered pages.