Vue / Advanced: architecture, hydration, and production evidence / Lesson 16
Server rendering and deterministic hydration
Build request-owned server state and ensure the client starts from the same render-relevant contract.
On this page
Lesson goals
- Distinguish server rendering, client hydration, and browser-only effects.
- Prevent cross-request state leakage and avoidable mismatches.
Hydrate the tree that the server actually produced
Server-side rendering produces HTML from an application tree, while hydration attaches client behavior to the corresponding existing markup. The client’s initial render-relevant state and structure must agree with the server output. Hydration is not simply mounting a different application over arbitrary HTML. SSR can improve some delivery and search requirements but introduces server execution, serialization, caching, and request-isolation responsibilities. Choose it for the product’s needs and use a supported integration rather than assembling a production renderer from one short snippet. This course describes the boundaries without starting an SSR server or contacting any service.
Create fresh mutable context for each request
A server process can handle many users while imported modules remain cached. A shared mutable singleton can therefore leak one request’s state into another. The factory below creates a fresh app and Pinia instance each time it is called. A full integration also needs a request-owned router where used, data loading, rendering, state transfer, and error handling. Authentication and private data must stay within the correct request scope. Do not export one initialized mutable store and reuse it for every server request merely because that pattern worked in a client-only application.
import { createSSRApp } from "vue"
import { createPinia } from "pinia"
import App from "./App.vue"
export function createRequestApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
Requires Vue, compatible Pinia, compiled App.vue, and a separate supported SSR integration. Each call allocates new mutable app/store context. No renderer, HTTP server, request, or cross-user runtime test was executed here.
Keep initial output deterministic
Random values, current time, locale differences, browser dimensions, and client-only storage can make the first client tree differ from the server. Transfer a reviewed initial snapshot or render a neutral shared placeholder until a browser-only effect is appropriate. Invalid HTML nesting can also be repaired by the browser into a different tree. Stable ids such as Vue 3.5 useId help within their documented application context, but they do not fix arbitrary state divergence. Identify the first mismatched source and its owner instead of suppressing every hydration warning. Treat server and client timezones and formatting policies explicitly.
Verify a shared initial-view calculation locally
The pure example creates a deterministic view model from supplied synthetic data on both sides. It does not embed JSON into HTML, run Vue, or perform hydration. Its value is proving that the chosen calculation has no hidden time or browser input for this case. Real state transfer needs a supported serializer that safely crosses the HTML boundary; JSON.stringify alone is not a universal secure embedding strategy. Keep private fields out of public serialized state and validate the received contract before use. A byte-equal view model still needs actual markup and browser integration checks.
function initialView(record) {
return { id: record.id, title: record.title, expanded: false }
}
const payload = { id: "n1", title: "Notes" }
const serverView = initialView(payload)
const clientView = initialView(JSON.parse(JSON.stringify(payload)))
if (JSON.stringify(serverView) !== JSON.stringify(clientView)) {
throw new Error("INITIAL_VIEW_MISMATCH")
}
Runs under Node using local plain data only. The assertion checks this shared calculation. It is not an SSR, HTML serialization, DOM, or hydration test, and no production state is read.
Treat hydration timing and mismatch handling deliberately
Browser mount hooks do not run during ordinary server rendering, so browser listeners and timers should not be created in shared setup expecting client-style unmount cleanup on the server. Vue 3.5 supports specific lazy-hydration strategies for async components and a documented mismatch-suppression escape hatch, but neither is a blanket repair for inconsistent data. Delayed interactivity changes the user contract and must be tested. Diagnose source state, valid HTML, compiler/runtime compatibility, and request isolation before choosing a targeted exception. Verify representative direct entries and multiple synthetic request contexts in an authorized SSR test environment.
Common errors and fixes
Hydration starts from random client-only state
- Why it happens
- The initial server/client render contract differs.
- How to fix it
- Transfer deterministic state or use an intentional shared placeholder.
- Prevent it next time
- Test first render before browser-only effects.
One user’s state appears in another response
- Why it happens
- Mutable module state is shared across requests.
- How to fix it
- Create fresh request-owned app and data contexts.
- Prevent it next time
- Verify independent synthetic request isolation.
Warnings are hidden without fixing divergence
- Why it happens
- Suppression is treated as correctness.
- How to fix it
- Trace the mismatched state or HTML and justify any narrow exception.
- Prevent it next time
- Preserve mismatch evidence during diagnosis.
Check your understanding
1. A server template uses the current date while the client formats its own current date. What can diverge?
Show worked solution
Time, timezone, locale, and elapsed time can produce different text or structure. Define a shared timestamp and formatting policy or defer the browser-specific view behind a neutral matching initial state, then test actual hydration.
2. A pure initial-view test passes. Does that verify SSR isolation?
Show worked solution
No. It checks one deterministic calculation. Separate request-owned application and store tests must show that mutable state does not cross users, while actual SSR/browser tests verify markup and hydration behavior.
Key takeaways
- Hydration requires agreement on initial state and structure.
- Mutable server context belongs to each request.
- Mismatch suppression and delayed hydration need narrow justified use.