Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
4.1 KiB
Markdown
170 lines
4.1 KiB
Markdown
# Use Suspense Events for Loading State Tracking
|
|
|
|
## Rule
|
|
|
|
`<Suspense>` emits three events: `pending`, `resolve`, and `fallback`. Use these events to track loading states, implement analytics, coordinate UI updates, or trigger side effects at appropriate times.
|
|
|
|
## Why This Matters
|
|
|
|
Relying solely on the fallback slot for loading indication limits your options. The events provide programmatic access to Suspense state changes, enabling:
|
|
- Loading progress tracking
|
|
- Analytics for perceived performance
|
|
- Coordinated animations
|
|
- Custom loading indicators outside the Suspense boundary
|
|
- Debugging async component behavior
|
|
|
|
## Basic Usage
|
|
|
|
```vue
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
|
|
const loadingState = ref('idle')
|
|
const loadStartTime = ref(null)
|
|
|
|
const onPending = () => {
|
|
loadingState.value = 'pending'
|
|
loadStartTime.value = Date.now()
|
|
console.log('Suspense entered pending state')
|
|
}
|
|
|
|
const onResolve = () => {
|
|
loadingState.value = 'resolved'
|
|
const loadTime = Date.now() - loadStartTime.value
|
|
console.log(`Content resolved in ${loadTime}ms`)
|
|
|
|
// Track performance
|
|
analytics.track('page_load_time', { duration: loadTime })
|
|
}
|
|
|
|
const onFallback = () => {
|
|
loadingState.value = 'fallback'
|
|
console.log('Fallback content is now visible')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Suspense
|
|
@pending="onPending"
|
|
@resolve="onResolve"
|
|
@fallback="onFallback"
|
|
>
|
|
<AsyncPage />
|
|
<template #fallback>
|
|
<LoadingSkeleton />
|
|
</template>
|
|
</Suspense>
|
|
</template>
|
|
```
|
|
|
|
## Advanced: Global Loading Indicator
|
|
|
|
```vue
|
|
<!-- App.vue -->
|
|
<script setup>
|
|
import { ref, provide } from 'vue'
|
|
|
|
const globalLoading = ref(false)
|
|
const pendingCount = ref(0)
|
|
|
|
provide('globalLoading', globalLoading)
|
|
|
|
const onSuspensePending = () => {
|
|
pendingCount.value++
|
|
globalLoading.value = true
|
|
}
|
|
|
|
const onSuspenseResolve = () => {
|
|
pendingCount.value--
|
|
if (pendingCount.value === 0) {
|
|
globalLoading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Global loading bar -->
|
|
<Transition name="fade">
|
|
<LoadingBar v-if="globalLoading" />
|
|
</Transition>
|
|
|
|
<RouterView v-slot="{ Component }">
|
|
<Suspense
|
|
@pending="onSuspensePending"
|
|
@resolve="onSuspenseResolve"
|
|
>
|
|
<component :is="Component" />
|
|
<template #fallback>
|
|
<PageSkeleton />
|
|
</template>
|
|
</Suspense>
|
|
</RouterView>
|
|
</template>
|
|
```
|
|
|
|
## Event Sequence
|
|
|
|
```
|
|
Initial Load:
|
|
1. @pending - Suspense enters pending state
|
|
2. @fallback - Fallback content is displayed (may not fire if resolved quickly)
|
|
3. @resolve - Async content is ready
|
|
|
|
On Content Change (with timeout):
|
|
1. @pending - New async dependency detected
|
|
2. (Previous content still visible during timeout period)
|
|
3. @fallback - Timeout exceeded, fallback shown
|
|
4. @resolve - New content ready
|
|
|
|
On Content Change (fast):
|
|
1. @pending - New async dependency detected
|
|
2. @resolve - Resolved before timeout, fallback never shown
|
|
```
|
|
|
|
## Coordinating with Transitions
|
|
|
|
```vue
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
|
|
const isTransitioning = ref(false)
|
|
|
|
const onPending = () => {
|
|
isTransitioning.value = true
|
|
}
|
|
|
|
const onResolve = () => {
|
|
// Delay to allow enter transition to complete
|
|
setTimeout(() => {
|
|
isTransitioning.value = false
|
|
}, 300)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="{ 'is-loading': isTransitioning }">
|
|
<Transition mode="out-in" @after-leave="onAfterLeave">
|
|
<Suspense @pending="onPending" @resolve="onResolve">
|
|
<component :is="currentView" :key="viewKey" />
|
|
<template #fallback>
|
|
<LoadingView />
|
|
</template>
|
|
</Suspense>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Key Points
|
|
|
|
1. `@pending` fires when Suspense starts waiting for async dependencies
|
|
2. `@fallback` fires when fallback content becomes visible (respects timeout)
|
|
3. `@resolve` fires when default slot content is ready to display
|
|
4. Use events for analytics, debugging, and coordinating UI outside Suspense
|
|
5. Events enable global loading indicators across multiple Suspense boundaries
|
|
6. `@fallback` may not fire if content resolves before the timeout
|
|
|
|
## References
|
|
|
|
- [Vue.js Suspense Events](https://vuejs.org/guide/built-ins/suspense#events)
|