Toaster 自身的 Toast 状态在使用 Toast() 方法写入时与根应用的顶级 await (包含 Suspense) 不同步
作者: niklasfjeldberg创建于 2026年7月4日更新于 2026年9月23日
标签needs reproductiontriage
Environment
- Nuxt: 4.4.8
- Nuxt UI: 4.9.0
- Vue: 3.5.39
- Nitro: 2.13.4
ssr: true- Reproduced in both
nuxt devand anitro-Cloudflare-modulepreset build served viawrangler dev(so it isn't a dev-only artifact of Vite's SSR pipeline).
Description
useToast().add() pushes an item into the shared toasts state correctly (confirmed by reading toasts.value back from the same useToast() call site, including after nextTick() and after a 300ms setTimeout), but the actually-mounted <Toaster> (rendered via <UApp>) never renders it — its own internal toasts binding stays [] forever.
I confirmed this precisely by walking the live Vue component instance tree from the rendered toast viewport element:
const ol = document.querySelector('ol[data-slot="viewport"]');
let cur = ol.__vueParentComponent;
while (cur) {
if (cur.setupState?.toasts !== undefined) {
console.log(cur.type?.name, cur.setupState.toasts.value ?? cur.setupState.toasts);
break;
}
cur = cur.parent;
// → "Toaster" []
}
...while a separate `useToast()` call in the same page (in `onMounted`, guaranteed no timing/inject warnings) shows the pushed item present in `toasts.value` the whole time. Two `useToast()` call sites that should share one `useState('toasts', ...)` instance are desynced — the write succeeds, the read (inside `Toaster`'s own setup) never sees it.
**Suspected trigger:** our root `app.vue` has a **top-level `await`** before `<UApp>` mounts (fetching page data needed for SEO/branding), making `app.vue` itself an async component that Nuxt wraps in a `<Suspense>` boundary. I strongly suspect this is what desyncs `Toaster`'s `useState('toasts')` binding from the rest of the app's calls — this matches a known class of Vue/Nuxt bug where nested async-`setup()` components under `<Suspense>` can end up with inconsistent `getCurrentInstance()`/injection context after resolution, but I did not have time to build a minimal isolated repro removing our app's other complexity to nail down whether the top-level await is necessary and sufficient.
Separately (and already fixed on our end, not what this issue is about): a component that calls `useToast()` (via a wrapper composable) *after* its own top-level `await` reliably logs `[Vue warn]: inject() can only be used inside setup() or functional components` — consistent with the known async-setup/lifecycle-timing footgun, and probably a contributing factor but not the sole cause, since the empty-`Toaster`-state repro above happens even with a `useToast()` call site that has *zero* Vue warnings (top of a fresh page's `onMounted`).
Related issues I found that circle similar territory but don't exactly match this (no top-level-await/Suspense angle, no resolution reached in either):
- https://GitHub.com/nuxt/ui/issues/3564 ("Toast not showing", `UApp` correctly wrapping, closed with no resolution)
- https://GitHub.com/nuxt/ui/issues/1477 (`useToast()` before `navigateTo()`,
…内容来源: nuxt/ui