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>
358 lines
7.6 KiB
Markdown
358 lines
7.6 KiB
Markdown
---
|
|
name: data-fetching-best-practices
|
|
description: Patterns and best practices for efficient data fetching in Nuxt
|
|
---
|
|
|
|
# Data Fetching Best Practices
|
|
|
|
Effective data fetching patterns for SSR-friendly, performant Nuxt applications.
|
|
|
|
## Choose the Right Tool
|
|
|
|
| Scenario | Use |
|
|
|----------|-----|
|
|
| Component initial data | `useFetch` or `useAsyncData` |
|
|
| User interactions (clicks, forms) | `$fetch` |
|
|
| Third-party SDK/API | `useAsyncData` with custom function |
|
|
| Multiple parallel requests | `useAsyncData` with `Promise.all` |
|
|
|
|
## Await vs Non-Await Usage
|
|
|
|
The `await` keyword controls whether data fetching **blocks navigation**:
|
|
|
|
### With `await` - Blocking Navigation
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Navigation waits until data is fetched (uses Vue Suspense)
|
|
const { data } = await useFetch('/api/posts')
|
|
// data.value is available immediately after this line
|
|
</script>
|
|
```
|
|
|
|
- **Server**: Fetches data and includes it in the payload
|
|
- **Client hydration**: Uses payload data, no re-fetch
|
|
- **Client navigation**: Blocks until data is ready
|
|
|
|
### Without `await` - Non-Blocking (Lazy)
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Navigation proceeds immediately, data fetches in background
|
|
const { data, status } = useFetch('/api/posts', { lazy: true })
|
|
// data.value may be undefined initially - check status!
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="status === 'pending'">Loading...</div>
|
|
<div v-else>{{ data }}</div>
|
|
</template>
|
|
```
|
|
|
|
Equivalent to using `useLazyFetch`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data, status } = useLazyFetch('/api/posts')
|
|
</script>
|
|
```
|
|
|
|
### When to Use Each
|
|
|
|
| Pattern | Use Case |
|
|
|---------|----------|
|
|
| `await useFetch()` | Critical data needed for SEO/initial render |
|
|
| `useFetch({ lazy: true })` | Non-critical data, better perceived performance |
|
|
| `await useLazyFetch()` | Same as lazy, await only ensures initialization |
|
|
|
|
## Avoid Double Fetching
|
|
|
|
### ❌ Wrong: Using $fetch Alone in Setup
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// This fetches TWICE: once on server, once on client
|
|
const data = await $fetch('/api/posts')
|
|
</script>
|
|
```
|
|
|
|
### ✅ Correct: Use useFetch
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Fetches on server, hydrates on client (no double fetch)
|
|
const { data } = await useFetch('/api/posts')
|
|
</script>
|
|
```
|
|
|
|
## Use Explicit Cache Keys
|
|
|
|
### ❌ Avoid: Auto-generated Keys
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Key is auto-generated from file/line - can cause issues
|
|
const { data } = await useAsyncData(() => fetchPosts())
|
|
</script>
|
|
```
|
|
|
|
### ✅ Better: Explicit Keys
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Explicit key for predictable caching
|
|
const { data } = await useAsyncData(
|
|
'posts',
|
|
() => fetchPosts(),
|
|
)
|
|
|
|
// Dynamic keys for parameterized data
|
|
const route = useRoute()
|
|
const { data: post } = await useAsyncData(
|
|
`post-${route.params.id}`,
|
|
() => fetchPost(route.params.id),
|
|
)
|
|
</script>
|
|
```
|
|
|
|
## Handle Loading States Properly
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data, status, error } = await useFetch('/api/posts')
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="status === 'pending'">
|
|
<SkeletonLoader />
|
|
</div>
|
|
<div v-else-if="error">
|
|
<ErrorMessage :error="error" />
|
|
</div>
|
|
<div v-else>
|
|
<PostList :posts="data" />
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Use Lazy Fetching for Non-critical Data
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const id = useRoute().params.id
|
|
|
|
// Critical data - blocks navigation
|
|
const { data: post } = await useFetch(`/api/posts/${id}`)
|
|
|
|
// Non-critical data - doesn't block navigation
|
|
const { data: comments, status } = useFetch(`/api/posts/${id}/comments`, {
|
|
lazy: true,
|
|
})
|
|
|
|
// Or use useLazyFetch
|
|
const { data: related } = useLazyFetch(`/api/posts/${id}/related`)
|
|
</script>
|
|
|
|
<template>
|
|
<article>
|
|
<h1>{{ post?.title }}</h1>
|
|
<p>{{ post?.content }}</p>
|
|
</article>
|
|
|
|
<section v-if="status === 'pending'">Loading comments...</section>
|
|
<CommentList v-else :comments="comments" />
|
|
</template>
|
|
```
|
|
|
|
## Minimize Payload Size
|
|
|
|
### Use `pick` for Simple Filtering
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data } = await useFetch('/api/users', {
|
|
// Only include these fields in payload
|
|
pick: ['id', 'name', 'avatar'],
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### Use `transform` for Complex Transformations
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data } = await useFetch('/api/posts', {
|
|
transform: (posts) => {
|
|
return posts.map(post => ({
|
|
id: post.id,
|
|
title: post.title,
|
|
excerpt: post.content.slice(0, 100),
|
|
date: new Date(post.createdAt).toLocaleDateString(),
|
|
}))
|
|
},
|
|
})
|
|
</script>
|
|
```
|
|
|
|
## Parallel Fetching
|
|
|
|
### Fetch Independent Data with useAsyncData
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data } = await useAsyncData(
|
|
'dashboard',
|
|
async (_nuxtApp, { signal }) => {
|
|
const [user, posts, stats] = await Promise.all([
|
|
$fetch('/api/user', { signal }),
|
|
$fetch('/api/posts', { signal }),
|
|
$fetch('/api/stats', { signal }),
|
|
])
|
|
return { user, posts, stats }
|
|
},
|
|
)
|
|
</script>
|
|
```
|
|
|
|
### Multiple useFetch Calls
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// These run in parallel automatically
|
|
const [{ data: user }, { data: posts }] = await Promise.all([
|
|
useFetch('/api/user'),
|
|
useFetch('/api/posts'),
|
|
])
|
|
</script>
|
|
```
|
|
|
|
## Efficient Refresh Patterns
|
|
|
|
### Watch Reactive Dependencies
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const page = ref(1)
|
|
const category = ref('all')
|
|
|
|
const { data } = await useFetch('/api/posts', {
|
|
query: { page, category },
|
|
// Auto-refresh when these change
|
|
watch: [page, category],
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### Manual Refresh
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data, refresh, status } = await useFetch('/api/posts')
|
|
|
|
async function refreshPosts() {
|
|
await refresh()
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Conditional Fetching
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const userId = ref<string | null>(null)
|
|
|
|
const { data, execute } = useFetch(() => `/api/users/${userId.value}`, {
|
|
immediate: false, // Don't fetch until userId is set
|
|
})
|
|
|
|
// Later, when userId is available
|
|
function loadUser(id: string) {
|
|
userId.value = id
|
|
execute()
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## Server-only Fetching
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Only fetch on server, skip on client navigation
|
|
const { data } = await useFetch('/api/static-content', {
|
|
server: true,
|
|
lazy: true,
|
|
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key],
|
|
})
|
|
</script>
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const { data, error, refresh } = await useFetch('/api/posts')
|
|
|
|
// Watch for errors if need event-like handling
|
|
watch(error, (err) => {
|
|
if (err) {
|
|
console.error('Fetch failed:', err)
|
|
// Show toast, redirect, etc.
|
|
}
|
|
}, { immediate: true })
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="error">
|
|
<p>Failed to load: {{ error.message }}</p>
|
|
<button @click="refresh()">Retry</button>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Shared Data Across Components
|
|
|
|
```vue
|
|
<!-- ComponentA.vue -->
|
|
<script setup lang="ts">
|
|
const { data } = await useFetch('/api/user', { key: 'current-user' })
|
|
</script>
|
|
|
|
<!-- ComponentB.vue -->
|
|
<script setup lang="ts">
|
|
// Access cached data without refetching
|
|
const { data: user } = useNuxtData('current-user')
|
|
|
|
// Or refresh it
|
|
const { refresh } = await useFetch('/api/user', { key: 'current-user' })
|
|
</script>
|
|
```
|
|
|
|
## Avoid useAsyncData for Side Effects
|
|
|
|
### ❌ Wrong: Side Effects in useAsyncData
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Don't trigger Pinia actions or side effects
|
|
await useAsyncData(() => store.fetchUser()) // Can cause issues
|
|
</script>
|
|
```
|
|
|
|
### ✅ Correct: Use callOnce for Side Effects
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
await callOnce(async () => {
|
|
await store.fetchUser()
|
|
})
|
|
</script>
|
|
```
|
|
|
|
<!--
|
|
Source references:
|
|
- https://nuxt.com/docs/getting-started/data-fetching
|
|
- https://nuxt.com/docs/api/composables/use-fetch
|
|
- https://nuxt.com/docs/api/composables/use-async-data
|
|
- https://nuxt.com/docs/api/composables/use-lazy-fetch
|
|
-->
|