Files
stack/v1/packages/mosaic/framework/skills/nuxt/references/best-practices-data-fetching.md
T

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>