Skip to the solution
MediumFrontend10 min read

How does <Suspense> work in Vue 3.5 after the 2024 stabilization?

1.1k views

The problem

Question presented to candidate: "I read that <Suspense> became stable with Vue 3.5 in 2024. Is that right? Either way, explain how it behaves in 3.5 when the user switches between async views: when does the fallback appear, what does timeout do, and how do nested boundaries behave?"

What a strong answer should cover:

  • Correct the premise: Vue 3.5 (released 3 September 2024) did not stabilize Suspense. The 3.5.44 runtime still logs that it is experimental and the guide still opens with an "Experimental Feature" warning; the 3.6 release candidate prints the same message.
  • After the first resolve, a boundary only goes pending again when the root node of #default is replaced, and by default it keeps the old content until the new tree resolves, with no fallback.
  • timeout: 0 shows the fallback immediately on a switch, N after N ms; the value is read when the boundary is created.
  • suspensible (added in 3.3) lets a nested Suspense hand its dependencies to the parent so both levels swap in one step.
  • Events pending / fallback / resolve, and that lazily loaded router components do not trigger Suspense at all.
  • What 3.5 actually changed: many Suspense bug fixes across the 3.5.x line, plus neighbouring features (lazy hydration, deferred Teleport), not a status change.

Clarifying questions expected:

  • "Are we talking about first load or switching between already-loaded views?"
  • "Are the views lazy routes, async components, or components with an async setup()?"

Code / implementation expected: Optional β€” a Suspense with timeout, or a nested suspensible boundary, with the observed sequence of events.

  • suspense
  • async
  • vue-3.5

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 10 sections

Read the code

VueSuspense with :timeout="50": the old page stays, then the fallback, then the new page
<script setup>
import { shallowRef, defineComponent, h } from "vue";

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const view = (name, ms) =>
  defineComponent({
    name,
    async setup() {
      await sleep(ms);
      return () => h("p", name);
    },
  });
const views = { A: view("Page A", 20), B: view("Page B", 150) };
const current = shallowRef(views.A);
const log = (e) => console.log(`  event: ${e}`);
</script>

<template>
  <button class="goB" @click="current = views.B">B</button>
  <Suspense :timeout="50" @pending="log('pending')" @fallback="log('fallback')" @resolve="log('resolve')">
    <component :is="current" />
    <template #fallback><p>Loading...</p></template>
  </Suspense>
</template>
VueNested Suspense with suspensible: layout and panel swap in one step
<script setup>
import { ref, shallowRef, defineComponent, h } from "vue";

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const view = (name, ms) =>
  defineComponent({
    name,
    async setup() {
      await sleep(ms);
      return () => h("span", `[${name}]`);
    },
  });

const layouts = { L1: view("Layout 1", 30), L2: view("Layout 2", 60) };
const layout = shallowRef(layouts.L1);
const panels = { One: view("Panel 1", 30), Two: view("Panel 2", 120) };
const panel = shallowRef(panels.One);
const log = (where, e) => console.log(`  ${where}: ${e}`);
</script>

<template>
  <button class="two" @click="layout = layouts.L2; panel = panels.Two">Swap both</button>
  <Suspense @pending="log('outer', 'pending')" @fallback="log('outer', 'fallback')" @resolve="log('outer', 'resolve')">
    <div :key="layout.name">
      <component :is="layout" />
      <Suspense suspensible @pending="log('inner', 'pending')" @fallback="log('inner', 'fallback')" @resolve="log('inner', 'resolve')">
        <component :is="panel" />
        <template #fallback><span>(inner loading)</span></template>
      </Suspense>
    </div>
    <template #fallback><p>(outer loading)</p></template>
  </Suspense>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 26 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

How do you make provide/inject type-safe?

Continue