skip to content

When combining Vue 3's `<Transition>`, `<KeepAlive>` and `<Suspense>` around a dynamic component, in what order do you nest them, and why?

level: seniorimportance: should knowfreq 30%

answer

  1. the docs give one order
  2. Suspense closest to the component
  3. switching must replace Suspense's root
  4. out-in waits before inserting

basics

~20 s

Nest them as Transition, then KeepAlive, then Suspense, with the dynamic component directly inside Suspense. That makes each component switch replace Suspense's root, so it can wait on the new view, while KeepAlive caches and Transition animates the actual component.

solid answer

~40 s

The Vue docs give one working order: `<Transition>` outermost, `<KeepAlive>` inside it, `<Suspense>` inside that, and the `<component :is>` directly in Suspense's default slot, with the fallback beside it. The reasoning: Suspense only reverts to pending when the **root of `#default`** is replaced, so the switching component must be that root. `<KeepAlive>` and `<Transition>` both look through a Suspense child to the component inside it, so KeepAlive caches the real views and Transition animates their swap. With `mode="out-in"`, Suspense waits for the leave animation to finish before inserting the resolved view. Put Suspense outside KeepAlive instead and its root never changes, so a newly switched async view is not treated as a revert and gets neither the fallback nor the old content while it loads.

code

vue · 23 lines
vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import UsersView from './UsersView.vue'
import BillingView from './BillingView.vue'

const current = shallowRef(UsersView)
</script>

<template>
  <nav>
    <button @click="current = UsersView">Users</button>
    <button @click="current = BillingView">Billing</button>
  </nav>

  <Transition mode="out-in">
    <KeepAlive>
      <Suspense>
        <component :is="current" />
        <template #fallback>Loading...</template>
      </Suspense>
    </KeepAlive>
  </Transition>
</template>

go deeper

for a junior

Recall the order from the docs: Transition, KeepAlive, Suspense, then the dynamic component.

for a middle

Explain the root-replacement rule and why it forces Suspense to wrap the switching component directly.

for a senior

Diagnose views that load without fallback or lose cache after reordering, and use suspensible nested boundaries for layouts with outer and inner async views.

for a principal

Encapsulate the combination in one shared view-host component so the order is defined once and an experimental API change touches one file.

## The setup A tabbed admin area switches between views with `<component :is="current">`. The team wants three things at once: - **Animation** between views, with `<Transition>`; - **Cached** views that keep their state when you switch back, with `<KeepAlive>`; - **Coordinated loading** for views that use async setup, with `<Suspense>`. The Vue guide states that the nesting order of these components matters and gives the order that makes all three work. ## The documented order ```vue <Transition mode="out-in"> <KeepAlive> <Suspense> <component :is="current" /> <template #fallback>Loading...</template> </Suspense> </KeepAlive> </Transition> ``` The guide shows it inside a router view's slot; the structure is the same for any dynamic component. ## Why each layer sits where it does 1. **Suspense closest to the component.** A resolved `<Suspense>` returns to the pending state only when the **root node of `#default`** is replaced. With `<component :is>` as that root, every switch is such a replacement, so the boundary waits on the incoming view's async setup and applies its keep-old-content or `timeout` behaviour. 2. **KeepAlive around Suspense.** `<KeepAlive>` caches component instances. When its child is a Suspense, it looks through it to the component in Suspense's content, so each view instance is cached and restored as you switch. 3. **Transition outermost.** `<Transition>` animates its single child entering and leaving. When that child is a Suspense, the transition hooks are applied to the Suspense's content, so the views themselves animate. With `mode="out-in"`, when the new view resolves, Suspense waits for the old view's leave animation to finish before moving the new content in. ## What goes wrong with other orders | Order | Effect | |---|---| | Suspense outside KeepAlive | Suspense's root is the KeepAlive, which never changes; a switch happens deeper in the tree, which does not revert a resolved boundary, so a newly loaded async view shows no fallback and no old content while it resolves | | Suspense outside Transition | the same problem: the root never changes, so switches bypass the boundary's pending handling | | KeepAlive outside Transition | KeepAlive's child is the Transition wrapper rather than the switching views, so the views are not the instances being cached | The first two rows follow directly from the root-replacement rule; the practical lesson is that Suspense must wrap the thing that switches, and the other two wrap Suspense. ## Nested boundaries and suspensible (3.3+) Layouts with an outer and an inner async component can use a second boundary: ```vue <Suspense> <component :is="OuterLayout"> <Suspense suspensible> <component :is="InnerPage" /> </Suspense> </component> </Suspense> ``` Without `suspensible`, the outer boundary treats the inner one like a synchronous component: the inner shows its own fallback, and switching both at once can cause empty nodes and extra patch cycles. With `suspensible`, all dependency handling, including events, goes to the outer boundary, and the inner one only marks where patching resolves. Nested Suspense is supported from Vue 3.3. ## Debugging a wrong order Symptoms map back to the layer that is misplaced: - **Switching to an async view shows neither fallback nor old content** until it resolves: Suspense is not wrapping the switching component directly, so the switch is not a root replacement. - **Views lose their state when you switch back**: KeepAlive is not wrapping the Suspense (or the component), so it is not caching the views. - **Old and new views overlap or jump during loading**: the Transition lacks `mode="out-in"`, or sits somewhere Suspense cannot coordinate with it. In each case, moving the layer back to the documented position fixes the symptom without further code. ## Things to check in review - The dynamic component is the **direct** root of Suspense's default slot. - `mode="out-in"` is set when views must not overlap; Suspense cooperates with it. - The fallback slot has one root node. - Cached async views run their async setup only on first creation; reactivating a cached view does not re-run setup. ## The interview answer in one line Transition, KeepAlive, Suspense, component: Suspense must wrap the thing that switches so each switch is a root replacement, and the other two see through Suspense to the real views.

  • What does the suspensible prop on an inner Vue 3 <Suspense> change?
    It makes the inner boundary hand all dependency handling, including its events, to the parent boundary; the inner one only serves as a boundary for resolution and patching. Without it, the inner Suspense acts like a synchronous component with its own fallback, which can cause empty nodes and extra patch cycles when outer and inner views switch together. It requires Vue 3.3+.
  • Why does mode="out-in" work well with Vue 3 <Suspense>?
    When the new view resolves and the outgoing content has an out-in transition, Suspense waits for the leave animation to finish before moving the new content in. The two views never overlap, and the enter animation starts on a view that is fully ready.

saying these in an interview costs you the question

  • The order of Transition, KeepAlive and Suspense does not matter
  • Suspense should be outermost so it covers everything
  • Any change deep inside Suspense reverts it to pending
  • KeepAlive cannot cache components rendered inside Suspense
  • Nested Suspense works the same on every Vue 3 version