Nuxt 3 Nitro SSR ↔ Vue 3 Client: useState Reactivity Preservation Across Hydration Boundary
0 reputation · 29 Sept 2023, 14:57 UTC
0 reputation · 29 Sept 2023, 14:57 UTC
Nuxt 3 delegates server‑side rendering to the Nitro engine while the Vue 3 client takes over after hydration. Data fetched via useAsyncData is serialized into the payload and rehydrated, but the framework also encourages sharing reactive state through useState composables that can be instantiated on the server and reused on the client.
The memory‑leak fix introduced in Nuxt 3.4.0 (clearContext hook) ensures request‑scoped data is released after each SSR response. However, the contract for nested useState instances that survive the SSR‑to‑client transition remains unspecified. In particular, it is unclear whether reactivity is guaranteed when a parent useState holds a reference to a child useState created during server rendering, then the same composable tree is accessed during client‑side navigation without a full page reload.
ssr: true, dev: false)hydrate or dehydrate calls in user codeDoes the current implementation preserve deep reactivity for nested useState trees across the hydration boundary? Are there documented lifecycle hooks that guarantee re‑initialization or proxy replacement on the client? What is the expected behavior when a navigated‑to page reuses a useState key that was populated during the initial SSR pass?
30775 reputation · 29 Sept 2023, 22:32 UTC
For the documented usage pattern, yes: a useState value is deeply reactive after hydration, and it persists across client-side navigation. But the nested case you describe — a parent useState holding a reference to a child ref object created during SSR — is not guaranteed to stay reactive. The supported pattern is plain, serializable values under separate keys, each accessed through its own useState call on the client.
useState returns a deep Vue ref. During SSR, its value is written into the per-request payload state, serialized into the HTML (Nuxt uses devalue for this), and restored into the client-side state map before the app mounts. The client ref therefore starts with the server-rendered value and remains a normal deep ref — mutations to nested plain objects and arrays trigger re-renders as usual.
The key limit is serialization. devalue round-trips plain objects, arrays, and built-ins like Map, Set, Date, and RegExp, and it preserves shared references between plain objects. It cannot restore functions, class instances, or live ref/computed objects as functioning reactive objects. So if your parent's value embeds the child ref itself rather than the child's plain value, that ref arrives on the client as inert data. The child composable, when called again on the client, returns a fresh ref from the restored state map — not the serialized object — so the parent's embedded copy and the live child ref silently diverge.
The client state map persists across client-side navigation. When a navigated-to page calls useState with a key already populated during the initial SSR pass, the initializer is skipped and the existing value is reused — it survives until a full page reload delivers a fresh payload. This is documented behavior, and it is usually what you want; if you need per-navigation refresh, do it explicitly (e.g., reassign in a page setup or use useAsyncData with a key and refresh()).
The request-scoped cleanup shipped around Nuxt 3.4.0 releases server-side per-request context after each SSR response. It operates entirely on the server and neither creates nor destroys client-side reactivity — so it is not part of the hydration contract you're asking about. (I could not confirm the exact clearContext hook name from memory; check the 3.4.0 release notes before citing it.)
There is no documented Nuxt hook that re-initializes or replaces individual useState proxies on the client. Restoration is implicit during hydration; general app hooks such as app:rendered and app:mounted bracket the process but give no per-key guarantees. Treat any reliance on the client ref being backed by the shared payload object, or on cross-key reference identity surviving hydration, as an implementation detail that may change between minor releases.
nuxi build && node .output/server/index.mjs) with ssr: true.If your composable tree genuinely needs a parent to track a child, store the child's key (a string) in the parent and resolve it via useState(childKey) wherever needed. That keeps every reactive value inside the serialization-safe state map.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.