963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
7.9 KiB
7.9 KiB
name, description
| name | description |
|---|---|
| data-fetching-best-practices | 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
<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)
<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:
<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
<script setup lang="ts">
// This fetches TWICE: once on server, once on client
const data = await $fetch('/api/posts');
</script>
✅ Correct: Use useFetch
<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
<script setup lang="ts">
// Key is auto-generated from file/line - can cause issues
const { data } = await useAsyncData(() => fetchPosts());
</script>
✅ Better: Explicit Keys
<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
<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
<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
<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
<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
<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
<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
<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
<script setup lang="ts">
const { data, refresh, status } = await useFetch('/api/posts');
async function refreshPosts() {
await refresh();
}
</script>
Conditional Fetching
<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
<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
<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
<!-- 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
<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
<script setup lang="ts">
await callOnce(async () => {
await store.fetchUser();
});
</script>