Fix: Suspense Slots Expect a Single Root Node in Vue.js

Error message:
<Suspense> slots expect a single root node.
Components 2025-01-25

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