Vue/Nuxt SSR `onServerPrefetch` timing issue
Context
In a Nuxt 4 / Vue 3 SSR app, I have data that is fetched in a child component’s
onServerPrefetch() and written into a shared state (useState('page-context')).
However, I want to display that state in a parent component — for example in
app.vue or a layout — at render time on the server.
Example:
What happens
- On the server, the HTML produced containsContext: {} (empty object).
- On the client after hydration, the reactive state now includes{ fromPageContext: "set in pages/index.vue prefetch" }.
- This causes a hydration text mismatch because the server HTML and
the hydrated state differ.
Console warning:
What I tried
- Wrapping <NuxtPage> with <Suspense> in app.vue → no effect (Nuxt already
uses internal Suspense around pages).
- Waiting for app:rendered hook in a plugin → blocks forever (app already
started rendering).
- Tracking global prefetch state manually → SSR doesn’t emit updates mid-render.11 Replies
Hi @ChronicStone. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
----------------------
You're encountering a classic SSR timing issue in Nuxt: parent components render before child components have completed their
Move your data fetching to the parent or a higher level to ensure the state is available during SSR. There is no built-in way in Nuxt/Vue to "wait" for a child’s
onServerPrefetch hooks, so shared state set in a child is not available to the parent during the initial server render. This leads to a hydration mismatch, as the server-rendered HTML doesn't match the client state after hydration.
Why this happens
- In SSR, parent components render first. When rendering the parent, the child’s onServerPrefetch hasn’t run yet, so the shared state is still empty.
- After all onServerPrefetch hooks resolve, Nuxt re-renders the affected components, but the parent is not re-rendered with the updated state—only the child (and its subtree) is.
- On the client, the state is set before hydration, so the mismatch occurs.
This is a known limitation of Vue/Nuxt SSR and is discussed in best practices and common hydration mismatch scenarios in the Nuxt docs. The docs specifically mention that inconsistent data between server and client is a common cause of hydration errors, and recommend using SSR-friendly state composables like useState—but this only works if the state is set before the parent renders on the server, which is not the case with onServerPrefetch in a child component Nuxt 4 Hydration Best Practices.
Workarounds
#### 1. Lift the data fetching up
Move the data fetching to the parent (e.g., layout or app.vue) so the state is set before the parent renders. This ensures the state is available during the initial server render.
#### 2. Use SSR-friendly composables in the parent
If you must fetch data in the child, consider using useAsyncData or useFetch in the parent, or pass the data down as props, so the parent has the data at render time.
#### 3. Delay parent rendering until state is ready
Nuxt does not provide a built-in way to suspend the parent until a child’s onServerPrefetch completes. Wrapping <NuxtPage> in <Suspense> does not help, as you’ve found, because Nuxt already handles suspense internally.
#### 4. Show a fallback or loading state
If you cannot restructure your data flow, you can show a loading or fallback state in the parent until the shared state is populated, to avoid mismatches.
Why you can't "fix" this with hooks or plugins
- The app:rendered hook is called after the app is rendered, so it’s too late to update the state for SSR output Nuxt 4 Lifecycle.
- SSR does not emit updates mid-render; the render is a one-pass process Nuxt 4 Lifecycle.
Summary
You cannot reliably set shared state in a child’s onServerPrefetch and expect it to be available in a parent during SSR. The only robust solution is to fetch the data in the parent or at a higher level, so the state is available when the parent renders on the server.
For more on this, see the Nuxt docs on hydration and SSR data flow Nuxt 4 Hydration Best Practices and SSR lifecycle Nuxt 4 Lifecycle.
If you need advanced control over hydration, Nuxt provides useHydration, but this is intended for plugins and advanced use cases, not for general component data flow useHydration composable.
---
TL;DR:Move your data fetching to the parent or a higher level to ensure the state is available during SSR. There is no built-in way in Nuxt/Vue to "wait" for a child’s
onServerPrefetch before rendering the parent on the server.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@372104941348585473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@kapa.ai
Let me check the knowledge base for you again. One moment! :books:
Sorry @ChronicStone, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist:
Perhaps try again in 15mins?
@kapa.ai
It's not a "common" pattern either, it has one very specific use case. On my app layout, there are two things at the top :
Page title & breadcrumbs. These are defined in page meta
definePageMeta({
breadcrumbs: ({ ctx, route: router, localePath }) => [
{
label:
getLocalizedValue(ctx.translations ?? [], 'name')
?? router.params.itemShortId,
route: localePath({ name: 'item-itemShortId' }),
params: { itemShortId: router.params.itemShortId }
}
]
})
As you can see, there is a ctx property, this ctx can be set lower in the tree by components fetching data.
Can't I put the breadcrumbs, title ...etc at the end of my layout & use a deferred teleport to render them at the top ?
Let me check the knowledge base for you again. One moment! :books:
----------------------
Based on the provided knowledge sources, using a deferred
While you can technically use
<Teleport> to render content (like breadcrumbs or page title) at the top of your layout is technically possible in Nuxt, but there are important caveats:
- Nuxt supports the Vue <Teleport> component for moving content elsewhere in the DOM. On the server, Nuxt only supports teleporting to #teleports. For other targets, you must wrap your teleport in a <ClientOnly> component, which means the content will only appear after hydration on the client side (Teleport docs).
- SSR Limitation: If you use <Teleport> (especially with <ClientOnly>), the teleported content (like your breadcrumbs) will not be present in the server-rendered HTML. This means users and crawlers will not see the breadcrumbs until after hydration, which can impact SEO and user experience.
- Data Flow: The pattern you describe—setting ctx lower in the tree and then rendering meta-dependent UI at the top—runs into the same SSR timing issue as before. The server renders the layout before the child components have set the necessary context, so the teleported content will not have the correct data on the initial render.
Example from the docs:
(Teleport API docs)
Summary:While you can technically use
<Teleport> (with <ClientOnly>) to move content in the DOM, this does not solve the SSR data timing issue. The teleported content will only be rendered on the client, after hydration, and will not be available in the initial server-rendered HTML. If SEO or immediate visibility is important, this approach is not recommended.
If you need the breadcrumbs and title to be available on the server-rendered HTML, you should ensure the data is available at the layout level before rendering, rather than relying on deferred rendering or teleportation.
If you need more advanced SSR data flow, the knowledge sources do not provide a built-in Nuxt solution for this specific pattern.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@372104941348585473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
I don't want <ClientOnly /> @kapa.ai
Let me check the knowledge base for you again. One moment! :books:
----------------------
If you do not want to use
- You can use
<ClientOnly />, then you are limited by Nuxt's SSR support for <Teleport>. According to the official documentation, Nuxt currently only supports SSR teleports to #teleports. If you teleport to any other target, it will only work on the client and requires <ClientOnly /> (Teleport API docs).
Here’s the relevant excerpt:
Nuxt currently has SSR support for teleports toSo, if you want your teleported content (like breadcrumbs or page title) to appear in the server-rendered HTML, you must teleport to#teleportsonly, with client-side support for other targets using a<ClientOnly>wrapper.
#teleports. If you need to teleport to another selector (e.g., a custom header area), this is not supported on the server and will not work without <ClientOnly />.
In summary:- You can use
<Teleport to="#teleports"> for SSR, but not for arbitrary selectors.
- Teleporting to other selectors without <ClientOnly /> is not supported on the server.
- There is no built-in way in Nuxt to defer rendering to the top of your layout via teleportation and have it SSR-rendered, unless you use #teleports as the target.
If your use case requires SSR and you do not want <ClientOnly />, you will need to ensure your data is available at the layout level before rendering, rather than relying on teleportation from child components (Teleport API docs).
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------<@372104941348585473> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Can you, with ascii chars, make me a visual representation of the async render tree in ssr & what order what renderes in ...Etc