What Causes This Warning?
This warning occurs when <Suspense> contains multiple root elements or no elements in its default or fallback slots. Like <Transition> and <KeepAlive>, <Suspense> expects exactly one root node in each slot.
The Problem
<!-- ❌ Multiple root nodes in default slot -->
<template>
<Suspense>
<AsyncComponent />
<AnotherComponent />
</Suspense>
</template>
<!-- ❌ Multiple root nodes in fallback -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<p>Loading...</p>
<Spinner />
</template>
</Suspense>
</template>
The Fix
Wrap Multiple Elements
<!-- ✅ Single root wrapper -->
<template>
<Suspense>
<div>
<AsyncComponent />
<AnotherComponent />
</div>
</Suspense>
</template>
<!-- ✅ Both slots with single root -->
<template>
<Suspense>
<template #default>
<div>
<AsyncComponent />
<AnotherComponent />
</div>
</template>
<template #fallback>
<div class="loading-state">
<p>Loading...</p>
<Spinner />
</div>
</template>
</Suspense>
</template>
Using Fragments (Vue 3.3+)
<!-- ✅ Fragment component as wrapper -->
<script setup>
import { Fragment } from 'vue'
</script>
<template>
<Suspense>
<Fragment>
<AsyncComponent />
<AnotherComponent />
</Fragment>
</Suspense>
</template>
Common Scenarios
Async Setup Components
<!-- Parent.vue -->
<template>
<Suspense>
<!-- ✅ Single async component -->
<AsyncChild />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
<!-- AsyncChild.vue -->
<script setup>
// This makes the component async
const data = await fetchData()
</script>
<template>
<div>
<h1>{{ data.title }}</h1>
<p>{{ data.content }}</p>
</div>
</template>
Multiple Async Components
<!-- ❌ Wrong: multiple async components -->
<template>
<Suspense>
<UserProfile />
<UserPosts />
</Suspense>
</template>
<!-- ✅ Correct: wrap in container -->
<template>
<Suspense>
<div class="user-page">
<UserProfile />
<UserPosts />
</div>
<template #fallback>
<PageSkeleton />
</template>
</Suspense>
</template>
Nested Suspense
<!-- ✅ Nested Suspense for independent loading states -->
<template>
<Suspense>
<div class="layout">
<header>
<Suspense>
<UserNav />
<template #fallback>
<NavSkeleton />
</template>
</Suspense>
</header>
<main>
<Suspense>
<PageContent />
<template #fallback>
<ContentSkeleton />
</template>
</Suspense>
</main>
</div>
</Suspense>
</template>
With defineAsyncComponent
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent(() =>
import('./Modal.vue')
)
</script>
<template>
<Suspense>
<!-- ✅ Single async component -->
<AsyncModal v-if="showModal" />
<template #fallback>
<div class="modal-loading">Loading...</div>
</template>
</Suspense>
</template>
Router View with Suspense
<template>
<RouterView v-slot="{ Component }">
<Suspense>
<!-- ✅ Single component from router -->
<component :is="Component" />
<template #fallback>
<PageLoader />
</template>
</Suspense>
</RouterView>
</template>
Error Handling
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((e) => {
error.value = e
return false // Prevent propagation
})
</script>
<template>
<div v-if="error" class="error">
Something went wrong: {{ error.message }}
</div>
<Suspense v-else>
<AsyncComponent />
<template #fallback>
<LoadingState />
</template>
</Suspense>
</template>
Suspense Events
<template>
<Suspense
@pending="onPending"
@resolve="onResolve"
@fallback="onFallback"
>
<AsyncComponent />
<template #fallback>
<Loading />
</template>
</Suspense>
</template>
<script setup>
function onPending() {
console.log('Async operation started')
}
function onResolve() {
console.log('Async operation completed')
}
function onFallback() {
console.log('Showing fallback content')
}
</script>
Quick Checklist
- Each Suspense slot needs exactly one root element
- Wrap multiple elements in a container
<div> - Use separate Suspense boundaries for independent loading
- Handle errors with
onErrorCaptured - Use Suspense events for loading state tracking
- Consider nested Suspense for complex layouts