Files
gt-agent-company/.cursor/skills/vue/references/advanced-async-suspense.md
sexygoat 3c62cc1cd1 first
2026-03-31 18:41:52 +08:00

5.1 KiB

name, description
name description
async-components-suspense Handle async operations, top-level await, and Suspense boundaries

Async Components & Suspense

Vue provides patterns for handling asynchronous operations in components.

Top-Level await in Script Setup

<script setup> supports top-level await, making the component an async dependency:

<script setup lang="ts">
const data = await fetch('/api/data').then(r => r.json())
</script>

<template>
  <div>{{ data }}</div>
</template>

Important: Components with top-level await require a <Suspense> boundary in a parent component, otherwise they won't render.

Suspense

<Suspense> is a built-in component for handling async dependencies in the component tree.

<template>
  <Suspense>
    <!-- Component with async setup -->
    <AsyncComponent />
    
    <!-- Fallback while loading -->
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

Suspense Slots

  • default: The async content to render
  • fallback: Content shown while async dependencies are resolving
<Suspense>
  <template #default>
    <Dashboard />
  </template>
  <template #fallback>
    <LoadingSpinner />
  </template>
</Suspense>

Async Components

Define components that are loaded asynchronously:

import { defineAsyncComponent } from 'vue'

const AsyncModal = defineAsyncComponent(() => 
  import('./components/Modal.vue')
)

// With options
const AsyncModalWithOptions = defineAsyncComponent({
  loader: () => import('./components/Modal.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,        // Delay before showing loading (ms)
  timeout: 3000      // Timeout before showing error (ms)
})

Common Async Pitfalls

Pitfall 1: Missing Suspense Boundary

<!--  Won't work - no Suspense boundary -->
<template>
  <AsyncComponent />
</template>

<!--  Works -->
<template>
  <Suspense>
    <AsyncComponent />
    <template #fallback>Loading...</template>
  </Suspense>
</template>

Pitfall 2: Lifecycle Hooks After await

Lifecycle hooks must be registered synchronously, before any await:

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'

// ✅ Register hooks BEFORE await
onMounted(() => console.log('mounted'))
onUnmounted(() => console.log('unmounted'))

// Now you can await
const data = await fetchData()
</script>
<script setup lang="ts">
// ❌ WRONG - hooks after await won't work
const data = await fetchData()

onMounted(() => {
  // This may not be called!
})
</script>

Pitfall 3: Composables After await

Composables that use lifecycle hooks must be called before await:

<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'

// ✅ Call composables BEFORE await
const { x, y } = useMouse()

const data = await fetchData()
</script>

Pitfall 4: Watchers Created in Async Callbacks

Watchers in async callbacks aren't auto-disposed:

// ❌ Memory leak - watcher not auto-disposed
setTimeout(() => {
  watch(source, callback) // Must be stopped manually
}, 1000)

// ✅ Watchers in setup are auto-disposed
watch(source, callback)

Often better to handle async in onMounted or with watchers:

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const data = ref<Data | null>(null)
const isLoading = ref(true)
const error = ref<Error | null>(null)

onMounted(async () => {
  try {
    data.value = await fetchData()
  } catch (e) {
    error.value = e as Error
  } finally {
    isLoading.value = false
  }
})
</script>

<template>
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else>{{ data }}</div>
</template>

This pattern:

  • Doesn't require Suspense
  • Gives you control over loading/error states
  • Works everywhere without special parent setup

When to Use Suspense

Use Suspense when:

  • You have nested async components
  • You want coordinated loading states across multiple async children
  • You're doing SSR with async data requirements

Avoid Suspense when:

  • Simple single-component async loading
  • You need fine-grained control over loading states
  • You want to avoid Suspense complexity

Suspense Events

<Suspense
  @pending="onPending"
  @resolve="onResolve"
  @fallback="onFallback"
>
  <AsyncComponent />
</Suspense>

Error Handling with Suspense

Use onErrorCaptured or <ErrorBoundary> pattern:

<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'

const error = ref<Error | null>(null)

onErrorCaptured((e) => {
  error.value = e
  return false // Prevent propagation
})
</script>

<template>
  <div v-if="error">Error: {{ error.message }}</div>
  <Suspense v-else>
    <AsyncComponent />
    <template #fallback>Loading...</template>
  </Suspense>
</template>