What does Vue 3's built-in `<Suspense>` component do, and what belongs in its #default and #fallback slots?
answer
- one loading state for a subtree
- rendered in memory first
- two slots, one root each
- still marked experimental
basics
~20 s<Suspense> renders its #default content in memory and, while async dependencies inside it are unresolved, shows the #fallback slot instead; once all of them resolve it swaps the finished content in. It is still an experimental Vue feature.
solid answer
~40 s`<Suspense>` is a built-in component that coordinates **async dependencies** in the subtree under it: components with an async `setup()` (including `<script setup>` with top-level `await`) and async components. On the first render it renders `#default` off-screen; if it meets any unresolved dependency it enters a **pending** state and shows `#fallback`, typically a spinner or skeleton. When every dependency it found has resolved, it inserts the default content in one go. If there were none, it resolves immediately. Each slot allows **one immediate child node**. The Vue docs mark `<Suspense>` as experimental: the API may still change, and in development Vue logs a notice saying so the first time one is used.
code
vue · 13 lines<script setup lang="ts">
import Dashboard from './Dashboard.vue'
</script>
<template>
<Suspense>
<Dashboard />
<template #fallback>
<div class="skeleton">Loading dashboard...</div>
</template>
</Suspense>
</template>go deeper
Recall the two slots, default for real content and fallback for loading UI, and that async setup and async components are what Suspense waits for.
Explain the in-memory render of default, the pending and resolved states, and why an unawaited fetch is not a dependency.
Weigh the experimental label, keep Suspense usage contained, and plan error handling around it since it has no error slot.
Decide whether coordinated loading is worth an experimental API for the product, and set a boundary policy that can be swapped if the API changes.
## The problem it solves A page often contains several components that each need something asynchronous before they can render meaningfully: a profile that awaits the user record, a feed that awaits posts, a chart whose code is loaded lazily. Without coordination, each one shows its own spinner and content pops in piece by piece. `<Suspense>` gives the subtree **one** loading state and reveals the content **once**, when everything it is waiting for is ready. ## What it waits for `<Suspense>` only knows about two kinds of **async dependencies** below it: 1. **Components with an async `setup()`**. In `<script setup>`, any top-level `await` makes the component async automatically. 2. **Async components** created with `defineAsyncComponent`, which are "suspensible" by default when a `<Suspense>` is above them. A component that merely starts a `fetch` without awaiting it in setup is **not** a dependency; it renders immediately and handles its own loading state. ## The two slots ```vue <Suspense> <Dashboard /> <template #fallback> <p>Loading dashboard...</p> </template> </Suspense> ``` | Slot | Holds | Shown when | |---|---|---| | `#default` | the real content, which may contain async dependencies anywhere below it | once all dependencies found during rendering have resolved | | `#fallback` | the loading UI | while the boundary is pending | Both slots accept **one immediate child node**. With several, Vue renders an empty placeholder for that slot and, in development, warns `<Suspense> slots expect a single root node.`, so wrap multiple elements in one component or element. ## What happens on the first render 1. `<Suspense>` renders the `#default` content **in memory**, in a container that is not in the document. 2. If it meets any async dependency, it enters the **pending** state, emits `pending` and `fallback`, and mounts `#fallback` in the page. 3. Each dependency resolves on its own schedule; the boundary counts them down. 4. When the count reaches zero, it enters the **resolved** state: the fallback is removed, the finished default content is moved into the page, and it emits `resolve`. 5. If no dependency was met at all, it goes straight to resolved. Lifecycle hooks such as `onMounted` of components inside the pending content are held back and run when the boundary resolves, which is when their DOM actually enters the page. ## What a component with async setup looks like ```vue <script setup lang="ts"> const res = await fetch('/api/stats') const stats = await res.json() </script> <template> <StatsTable :rows="stats" /> </template> ``` This component cannot render until both awaits finish. Without a `<Suspense>` above it, Vue does not render it at all, and in development warns that a component with async `setup()` must be nested in a `<Suspense>`. ## Suspense versus a local loading flag The alternative to a boundary is the pattern most Vue code already uses: start a request without awaiting it and render a `v-if` loading branch inside the component. | | `<Suspense>` + async setup | Local loading flag | |---|---|---| | Where the loading UI lives | once, in the boundary's `#fallback` | in every component that loads | | When content appears | all together | each piece as its data arrives | | Setup code | linear, with top-level `await` | callbacks or a composable that exposes `pending` | | Stability of the API | experimental | stable building blocks | Neither is always better. A boundary suits a view whose pieces are meaningless on their own; local flags suit widgets that are useful as soon as each one loads. ## Experimental status The Vue guide opens the Suspense page with a warning: `<Suspense>` is an **experimental feature**, not guaranteed to reach stable status, and its API may change before it does. In development builds Vue prints `<Suspense> is an experimental feature and its API will likely change.` the first time a boundary is created. It works in production builds, but the status is a reason to keep its usage contained and to follow release notes. ## Key points to say in an interview - It coordinates **async setup** and **async components**, nothing else. - `#default` is rendered in memory first; `#fallback` shows while pending. - One root node per slot. - It is experimental, and it has no error slot; errors are handled with the error-capturing hooks of an ancestor.
- Does a component that calls fetch in setup without awaiting it make Vue 3's <Suspense> wait?No. Only an async `setup()`, such as top-level `await` in `<script setup>`, or an async component registers as a dependency. A promise started and not awaited leaves setup synchronous, so the component renders at once and must show its own loading state.
- What happens if you put two elements directly in a Vue 3 <Suspense> #fallback slot?Each slot expects one immediate child node. With two, Vue cannot pick a root, renders an empty placeholder for the slot and, in development, warns `<Suspense> slots expect a single root node.` Wrap the elements in a single element or component instead.
A restaurant kitchen that holds a table's order until every dish is ready, showing the diners bread in the meantime, instead of sending plates out one by one as each finishes.
saying these in an interview costs you the question
- Suspense waits for any promise created inside a component
- The fallback slot can hold several root elements freely
- Suspense is a stable, finalized Vue 3 API
- Suspense shows fallback once per async component, one at a time
- A component with top-level await renders fine without Suspense