Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/suspense-revert-only-on-root-change.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

3.5 KiB

Suspense Only Reverts to Pending on Root Node Replacement

Rule

Once resolved, <Suspense> only reverts to a pending state if the root node of the #default slot is replaced. New async dependencies nested deeper in the tree will NOT cause <Suspense> to revert to a pending state.

Why This Matters

This is a common source of confusion. Developers expect Suspense to show the fallback whenever any async component in the tree starts loading, but it only triggers for root-level changes. Nested async updates complete silently without showing any loading indicator.

Bad Code

<script setup>
import { ref } from 'vue';

const activeTab = ref('dashboard');
</script>

<template>
  <Suspense>
    <!-- This wrapper is always the same component -->
    <TabContainer>
      <!-- Tab changes won't trigger Suspense fallback! -->
      <AsyncDashboard v-if="activeTab === 'dashboard'" />
      <AsyncSettings v-else-if="activeTab === 'settings'" />
      <AsyncProfile v-else-if="activeTab === 'profile'" />
    </TabContainer>

    <template #fallback>
      Loading...
      <!-- Only shows on initial load -->
    </template>
  </Suspense>
</template>

Good Code

Solution 1: Use :key to Force Root Replacement

<script setup>
import { ref, defineAsyncComponent } from 'vue';

const activeTab = ref('dashboard');

const tabs = {
  dashboard: defineAsyncComponent(() => import('./Dashboard.vue')),
  settings: defineAsyncComponent(() => import('./Settings.vue')),
  profile: defineAsyncComponent(() => import('./Profile.vue')),
};
</script>

<template>
  <Suspense>
    <!-- Key change forces root replacement, triggering Suspense -->
    <component :is="tabs[activeTab]" :key="activeTab" />

    <template #fallback> Loading... </template>
  </Suspense>
</template>

Solution 2: Nested Suspense with suspensible Prop (Vue 3.3+)

<template>
  <Suspense>
    <TabContainer>
      <!-- Inner Suspense handles nested async components -->
      <Suspense suspensible>
        <AsyncDashboard v-if="activeTab === 'dashboard'" />
        <AsyncSettings v-else-if="activeTab === 'settings'" />
        <AsyncProfile v-else-if="activeTab === 'profile'" />

        <template #fallback>
          <TabLoadingSpinner />
        </template>
      </Suspense>
    </TabContainer>

    <template #fallback>
      <PageLoadingSkeleton />
    </template>
  </Suspense>
</template>

Solution 3: Manual Loading State for Nested Content

<script setup>
import { ref, watch } from 'vue';

const activeTab = ref('dashboard');
const isTabLoading = ref(false);

watch(activeTab, () => {
  isTabLoading.value = true;
});

const onTabLoaded = () => {
  isTabLoading.value = false;
};
</script>

<template>
  <Suspense>
    <TabContainer>
      <div v-if="isTabLoading" class="tab-loading">Loading tab...</div>
      <AsyncTab v-else :tab="activeTab" @vue:mounted="onTabLoaded" />
    </TabContainer>

    <template #fallback> Initial loading... </template>
  </Suspense>
</template>

Key Points

  1. Suspense tracks the root node identity, not the entire subtree
  2. Use :key to force root node replacement when you need Suspense to re-trigger
  3. For Vue 3.3+, nested <Suspense> with suspensible prop handles deeply nested async components
  4. Consider manual loading states for fine-grained control over nested content
  5. Without the suspensible prop, inner Suspense is treated as sync and causes multiple patching cycles

References