Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stabilize
<Suspense>by introducing explicit, composable primitives for async rendering:<script async setup>- Explicit opt-in for async component setup (replacing implicit detection of top-levelawait)v-deferdirective - Template-level promise unwrapping with deferred rendering<Suspense :with>prop - Explicit promise binding on Suspense boundaries with resolved value provision via scoped slotsThese three primitives address the core issues preventing Suspense stabilization: implicit async dependency detection, lack of visibility into what causes suspension, and inconsistent patterns between setup-phase and render-phase async handling.
Basic example
Pattern A: Self-contained async (
<Suspense :with>)The component creates promises and manages its own Suspense boundary. No async setup required.
Pattern B: Delegated async (
<script async setup>)The component declares itself as async. The parent provides the Suspense boundary.
Pattern C: Promise-as-prop (
v-defer)A reusable child component receives a Promise as a prop and defers its own rendering.
Motivation
Current problems with
<Suspense>1. Implicit async dependency detection
In the current implementation, any
<script setup>containing a top-levelawaitimplicitly becomes an async component and an async dependency of the nearest<Suspense>. This has several problems:awaitto a component silently changes how it renders and requires a<Suspense>ancestor. Removing theawaitsilently removes the dependency. This is easy to introduce or break accidentally.<Suspense>shows its fallback longer than expected, there is no straightforward way to identify which descendant(s) are still pending.2. No explicit connection between Suspense and its causes
The current
<Suspense>has no API for declaring what it is waiting for. It implicitly collects all async descendants. This makes the template unreadable - you cannot see at a glance what a particular<Suspense>is waiting for.3. Inconsistent async patterns
There are two fundamentally different phases where async can occur:
setup()is async (data fetching before first render)Current Vue only supports the setup-phase pattern via
<Suspense>. There is no built-in primitive for render-phase promise handling. Users resort to manualref+.then()patterns or third-party composables, which don't integrate with Suspense boundaries.4. Lack of composability
There is no standard way to pass a Promise through the component tree and have it integrate with Suspense. Patterns like "parent creates a promise, child renders the result" require manual wiring that is error-prone and doesn't participate in Suspense coordination.
Design goals
<Suspense>boundaries.Detailed design
1.
<script async setup>- Explicit async setupSyntax
The
asynckeyword on the<script setup>tag explicitly marks the component as having an async setup phase.Behavior
<Suspense>ancestor (same as current behavior for components with top-levelawait).asynckeyword, top-levelawaitin<script setup>produces a compiler error.Rationale
The
asynckeyword mirrors JavaScript's own requirement thatawaitcan only be used inside anasyncfunction. This makes the async nature of the component immediately visible without reading the entire setup body.2.
v-deferdirective - Template-level promise unwrappingSyntax
Where
expressionevaluates to aPromise<T>andidentifierbecomes a template variable of typeT(the resolved value).Usage on SFC root
<template>When the entire component should defer its rendering:
The component renders nothing until the promise resolves. It registers as an async dependency of the nearest
<Suspense>.Usage on inner elements
When only part of the template should be deferred:
Each
v-deferblock independently tracks its promise. The heading renders immediately; each deferred section appears when its promise resolves.When used on inner elements, each
v-deferregisters as a separate async dependency with the nearest<Suspense>. The Suspense shows its fallback until all deferred sections (and any async setup descendants) have resolved.Compilation
v-defercompiles to a conditional render guarded by the promise's resolution state. Conceptually:Promise reactivity
If the expression passed to
v-deferis reactive (e.g., a computed that returns a new Promise),v-deferresets its state and re-registers with Suspense when the promise changes. The previous resolved value is discarded and the section returns to the pending state.Error handling
If the promise rejects, the error propagates to the nearest
<Suspense>boundary'sonErrorhandler. See the Error Handling section below.3.
<Suspense :with>- Explicit promise bindingSyntax
Behavior
:withaccepts an object whose values are Promises.#fallbackslot until every promise resolves.#defaultslot as scoped slot props, keyed by the same names.v-deferdirectives). The Suspense resolves when all dependencies (both:withpromises and subtree dependencies) are settled.Type inference
TypeScript types flow through naturally:
Error handling
All three primitives integrate with a unified error handling model on
<Suspense>.onErrorevent#errorslot: A new named slot that renders when any tracked promise rejects. Receiveserror(the rejection reason) andretry(a function to re-execute all pending promises).@errorevent: Emitted when a promise rejects. Receives the error object. Useful for logging or side effects.Error sources:
:withpromise rejectsv-deferpromise in the subtree rejectsHow the three primitives compose
<script async setup>asynckeyword)v-defer<Suspense :with>Composition example
A realistic example combining all three primitives:
In this example:
App.vueuses:withto manage config loading (self-contained Suspense)AsyncDashboard.vueusesasync setupto fetch layout (delegated Suspense)MetricsPanel.vueusesv-deferto unwrap a promise prop (component-level deferral)SSR considerations
<script async setup>Behavior is unchanged from the current implementation. The server waits for the async setup to complete before rendering the component's template to HTML.
v-deferOn the server,
v-deferawaits the promise and renders with the resolved value. This ensures the server-rendered HTML includes the final content, matching what the client will hydrate to.If the promise rejects during SSR, the error propagates to the Suspense boundary or the SSR error handler.
<Suspense :with>On the server, Suspense awaits all
:withpromises before rendering the#defaultslot with the resolved values. The#fallbackslot is never rendered on the server.DevTools integration
To address the debugging difficulties of current Suspense:
v-deferdirective reports its promise state (pending/resolved/rejected) and the source expression to DevTools.<Suspense>components show a list of all tracked dependencies::withpromises (by key name),v-deferinstances (by source expression), and async setup components (by component name).asynckeyword on<script setup>is reflected in the component inspector.Drawbacks
<script setup>components with top-levelawaitmust add theasynckeyword. This is a mechanical change but is technically a breaking change.v-deferis a new directive: Adding a new built-in directive increases the learning surface. However,v-deferis only needed for the promise-as-prop pattern; the other two patterns cover the majority of use cases.v-deferon root<template>: Allowing a directive on the SFC root<template>tag is a new concept. However, it is syntactically natural and clearly communicates "this entire component is deferred."v-deferrequires new compilation logic for promise tracking, variable scoping (assyntax), and Suspense registration.Alternatives
1. Keep implicit async detection (status quo)
Do not require
<script async setup>. This avoids a breaking change but leaves the explicitness problem unsolved. Developers continue to be surprised by implicit Suspense dependencies.2.
use()hook instead ofv-defer(React-style)Provide a
use(promise)composable that suspends the component:This is simpler but less explicit in the template - you cannot see what is deferred by reading the template alone. It also conflates setup-phase and render-phase async into a single mechanism.
3.
<Await>component instead ofv-deferThis is viable and avoids adding a new directive. However:
v-if,v-for)4. Only
<Suspense :with>withoutv-deferRely solely on Suspense-level promise management. This covers many use cases but does not support the reusable "promise-receiving component" pattern where the child handles its own unwrapping.
5.
<Suspense :with>implicit shadowing (without scoped slot)Instead of requiring
<template #default="{ config }">to receive resolved values, the compiler could automatically shadow:withkeys in the default slot scope. The resolved values would replace the original Promise variables by name:This is more concise and eliminates the repetitive
<template #default="{ config }">wrapper. The compiler would::withobject (config)Awaited<T>(e.g.,Configinstead ofPromise<Config>)This approach is appealing for ergonomics but has trade-offs:
config) changes type depending on whether it's inside or outside the<Suspense>. This can be confusing when reading the template.v-if/v-for: Scope shadowing rules become complex when combined with other structural directives.#default="{ config }"makes it clear thatconfighas been transformed, which aligns with the RFC's goal of explicitness.This could be reconsidered as sugar syntax in a future iteration if the explicit pattern proves too verbose in practice.
6.
:causeprop for annotationThe earlier draft included a
:causeprop on<Suspense>for annotating the reason for suspension when using<script async setup>. This was dropped because:Adoption strategy
Migration from current Suspense
<script setup>withawait: Add theasynckeyword. This can be automated with a codemod that detects top-levelawaitin<script setup>blocks.Existing
<Suspense>usage: No changes required. Existing Suspense boundaries continue to work. The:withprop andv-deferare additive features.Deprecation period: The compiler can emit a warning (instead of an error) for top-level
awaitwithoutasyncfor one minor version cycle before making it an error.Teaching
:with: For most data-fetching scenarios,<Suspense :with>is the simplest and most self-contained pattern. Teach this first.async setupnext: For cases where the component itself needs to perform async initialization before rendering.v-deferlast: For advanced composition patterns where promises are passed through the component tree.Unresolved questions
v-deferwithout a<Suspense>ancestor: Should this be a compile-time warning, a runtime warning, or silently render nothing until resolved? The current proposal suggests a compile-time warning, but there may be valid use cases for standalonev-defer(e.g., progressive rendering without a loading state).Multiple
v-deferinteraction with Suspense: When a Suspense boundary has multiplev-deferdescendants, should the Suspense show fallback until ALL resolve, or should eachv-defersection independently appear? The current proposal says ALL must resolve (matching existing Suspense semantics), but per-section progressive rendering could be valuable.v-deferandv-if/v-forinteraction: How shouldv-defercompose with other structural directives? Priority order, nesting rules, and edge cases need to be specified. A likely rule:v-defercannot coexist withv-iforv-foron the same element (use a wrapping<template>instead).Promise identity and caching: When a reactive source produces a new Promise (same reference or different), should
v-deferre-suspend? The current proposal says yes (reset on new promise), but this may cause unnecessary fallback flashes. AkeepPreviousoption could be considered.Hydration mismatch: Since the server renders the resolved state and the client starts with the pending state, there is a potential hydration mismatch window. The hydration strategy for
v-deferneeds careful specification.<Suspense :with>reactivity: If a:withvalue is a ref that changes from one Promise to another, should Suspense re-suspend? How does this interact with<Suspense>'s existingtimeoutandsuspensibleoptions?