Data Fetching in Nuxt 3: A Comprehensive Guide

Updated on: 2025-08-20

Nuxt 3 introduces powerful data fetching composables that streamline building Vue applications with great performance and SEO. This guide explores useAsyncData, useLazyAsyncData, and useFetch, showing when and how to use each with practical examples.

Table of Contents

  1. Understanding Nuxt 3 Data Fetching
  2. useFetch
  3. useAsyncData
  4. useLazyAsyncData
  5. Comparison and Best Practices
  6. FAQs
  7. Conclusion

Understanding Nuxt 3 Data Fetching

Nuxt 3's data fetching system is designed to be intuitive and powerful. These composables integrate with SSR to render data on the server and hydrate on the client.

  • By default, useFetch and useAsyncData run during SSR and block navigation on client transitions until their handlers resolve.
  • If you don’t want to block navigation (for non‑critical or below‑the‑fold data), use useLazyAsyncData or set lazy: true.
  • $fetch alone is great for event‑based actions (like submitting a form), but for initial page data you want the SSR hydration and deduplication that the data composables provide.

useFetch

What is useFetch?

useFetch is ideal when you call HTTP endpoints (REST/GraphQL). It wraps useAsyncData and $fetch, giving you a simple API with SSR hydration and request deduplication.

Basic Usage

const { data, pending, error, refresh } = await useFetch("/api/users");

Key Features

  • Auto-imports and type hints for your API routes
  • Automatic request deduplication
  • Payload extraction for better performance
  • TypeScript support with automated type inference

Advanced Usage

const { data, pending, error, refresh } = await useFetch("/api/comments", {
  method: "post",
  body: { comment: "Great post!" },
  query: { postId: 1 },
  onRequest({ request, options }) {
    // Set the request headers
    options.headers = options.headers || {};
    options.headers.authorization = "...";
  },
  onRequestError({ request, options, error }) {
    // Handle the request errors
  },
  onResponse({ request, response, options }) {
    // Process the response data
  },
  onResponseError({ request, response, options }) {
    // Handle the response errors
  },
});

Common Use Cases for useFetch

  1. API Data Retrieval
const { data: posts } = await useFetch("/api/posts");
  1. Form Submissions
const { data, error } = await useFetch("/api/submit", {
  method: "POST",
  body: { name, email, message },
});
  1. Pagination
const page = ref(1);
const { data: paginatedPosts } = await useFetch(
  () => `/api/posts?page=${page.value}&limit=10`,
  { watch: [page] }
);
  1. Real-time Data Updates (Polling)
const { data: liveData, refresh } = await useFetch("/api/live-data");

const timer = setInterval(() => {
  refresh();
}, 5000);

onScopeDispose(() => {
  clearInterval(timer);
});

useAsyncData

What is useAsyncData?

useAsyncData is a flexible composable that lets you use any asynchronous function for data fetching. It’s great for combining multiple sources, custom clients/SDKs, or shaping payloads before they reach your component.

Basic Usage

const { data, pending, error, refresh } = await useAsyncData("users", () =>
  $fetch("/api/users")
);

Key Features

  • More control over the fetching process
  • Ability to use custom asynchronous functions
  • Options for lazy loading and server-only execution

useAsyncData blocks navigation until its async handler is resolved. Use lazy: true or useLazyAsyncData if you don’t want to block.

Common Use Cases for useAsyncData

  1. Complex Data Fetching
const { data: processedData } = await useAsyncData(
  "processedData",
  async () => {
    const [users, posts] = await Promise.all([
      $fetch("/api/users"),
      $fetch("/api/posts"),
    ]);
    return processData(users, posts);
  }
);
  1. Defaults and Transform (replace custom cache keys)
const { data: items } = await useAsyncData(
  "items", // key used for caching/invalidation
  () => $fetch("/api/items"),
  {
    default: () => [],
    transform: (list) => list.map((i) => ({ id: i.id, name: i.name })),
  }
);
  1. Server-Only Data Fetching
const { data: sensitiveData } = await useAsyncData(
  "sensitiveData",
  () => $fetch("/api/sensitive-data"),
  { server: true }
);
  1. Combining Multiple Data Sources
const { data: combinedData } = await useAsyncData("combinedData", async () => {
  const apiData = await $fetch("/api/data");
  const localData = await loadLocalData();
  return mergeData(apiData, localData);
});

Note on keys: The first argument (e.g., "items") is the unique key used for caching and invalidation. There is no separate cacheKey option.

useLazyAsyncData

What is useLazyAsyncData?

useLazyAsyncData is similar to useAsyncData, but it doesn't block navigation. It’s useful when you want to load data after the initial page load.

Basic Usage

const {
  pending,
  data: users,
  refresh,
} = useLazyAsyncData("users", () => $fetch("/api/users"));

useLazyAsyncData triggers navigation before the handler is resolved by setting the lazy option to true.

Common Use Cases for useLazyAsyncData

  1. Loading Non-Critical Data
const { data: additionalInfo } = useLazyAsyncData("additionalInfo", () =>
  $fetch("/api/additional-info")
);
  1. Infinite Scrolling
const page = ref(1);
const { data: posts, refresh } = useLazyAsyncData(
  "posts",
  () => $fetch(`/api/posts?page=${page.value}`),
  { watch: [page] }
);

const loadMore = () => {
  page.value++;
  refresh();
};
  1. User-Triggered Data Loading
const { data: userDetails, execute } = useLazyAsyncData(
  "userDetails",
  () => $fetch(`/api/user/${userId.value}`),
  { immediate: false }
);

const loadUserDetails = () => {
  execute();
};
  1. Conditional Data Fetching
const shouldFetch = ref(false);
const { data: conditionalData } = useLazyAsyncData(
  "conditionalData",
  () => (shouldFetch.value ? $fetch("/api/data") : Promise.resolve(null)),
  { watch: [shouldFetch] }
);

Comparison and Best Practices

ComposableBest Use CaseSSR BehaviorClient-Side Behavior
useFetchHTTP endpoints (REST/GraphQL)Fetches on server, caches resultUses cached data, refetches if needed
useAsyncDataComplex/custom async logicBlocks navigation until resolvedSame as server-side
useLazyAsyncDataNon-critical or user-triggeredDoesn't block navigationFetches data after component mount

Best Practices

  1. Use useFetch for HTTP endpoints you need to render the page
  2. Use useAsyncData for custom logic or when combining multiple sources
  3. Use useLazyAsyncData when you don’t want to block navigation
  4. Always handle loading and error states in your components
  5. Use the refresh function or refreshNuxtData(key) after mutations

FAQs

  1. Q: When should I use useFetch over useAsyncData?

A: Use useFetch when you’re calling an HTTP endpoint and want a simple API with SSR hydration and deduplication. Use useAsyncData for arbitrary async work (multiple sources, SDKs, or payload shaping).

  1. Q: How does Nuxt 3 handle SSR with these composables?

A: Nuxt executes these composables on the server during SSR, then serializes and transfers the result to the client, avoiding unnecessary refetching.

  1. Q: Can I use these composables with external APIs?

A: Yes, all these composables work with both internal and external API endpoints.

  1. Q: How do I handle authentication with these composables?

A: You can use the onRequest option to add authentication headers to your requests; on the server, forward cookies/headers using useRequestHeaders.

  1. Q: Is it possible to prefetch data for multiple pages?

A: Yes, you can use these composables in your layout files or implement custom prefetching logic using Nuxt's routing system.

Conclusion

Mastering Nuxt 3's data fetching composables helps you build performant, SEO-friendly applications. By understanding the strengths of useFetch, useAsyncData, and useLazyAsyncData, you can choose the right tool for each scenario in your Nuxt 3 projects.

Nuxt
Vue
SSR
Web Development