When developing high-performance Nuxt 3 applications, data fetching is one of the most crucial aspects to master. Whether you are loading initial page data, fetching API responses dynamically, or working with SDKs, understanding the differences between useFetch
, $fetch
, and useAsyncData
will greatly improve your app’s speed, SEO, and user experience.
In this guide, we explore each method in depth, compare their use cases, and uncover advanced techniques like lazy loading, caching, deduplication, and data transformation to help you build faster, smarter, and more scalable Nuxt 3 applications.
Understanding the Nuxt 3 Data Fetching Landscape
Nuxt 3 offers multiple composables and utilities for data fetching. Each serves a unique purpose depending on when and how the data is required:
-
useFetch()
: Best for server-side rendering (SSR) and automatic hydration via payloads. -
$fetch()
: Ideal for fetching data after page load, triggered by user actions. -
useAsyncData()
: Perfect for asynchronous operations involving SDKs or libraries instead of traditional REST endpoints.
By leveraging these tools correctly, you can minimize redundant requests, optimize page transitions, and ensure consistent SEO performance.
1. The Power of useFetch()
Server-Side Rendering and Payload Transfer
useFetch()
is designed for data fetching during server-side rendering. It runs the request once on the server and passes the data to the client through Nuxt’s payload mechanism. This means the client doesn’t have to refetch the same data, making your pages faster and SEO-friendly.
<script setup>
const { data } = await useFetch("https://dummyjson.com/api/endpoint");
</script>
This approach ensures that initial content is ready on page load, improving both performance and accessibility for users and search engines.
Blocking vs. Non-Blocking Navigation
Using await
makes navigation blocking until the data is fully loaded. While this guarantees ready-to-render content, it may slow down transitions. To enhance user experience, Nuxt offers two solutions:
-
Lazy Loading with
lazy: true
<script setup>
const { data, status } = await useFetch(
"https://dummyjson.com/api/endpoint",
{ lazy: true });
</script>
The page loads immediately, while the data populates asynchronously. You can display loading skeletons or placeholders during this time using:
<template v-if="status === 'pending'">
<SkeletonLoader />
</template>
- Use
useLazyFetch()
Instead of adding the lazy
option, simply switch to useLazyFetch()
for a cleaner syntax and non-blocking fetch behavior.
Automatic Re-fetching with Reactive Queries
useFetch()
supports reactive queries, enabling automatic data refresh when a reactive variable changes:
<script setup>
const userQuery = ref("");
const { data, status, execute } = await useFetch(
"https://dummyjson.com/api/users/search",
{
lazy: true,
query: { q: userQuery },
}
);
</script>
When userQuery
updates, the request re-runs automatically. You can also manually trigger a refresh using execute()
— ideal for “Refresh” buttons or dynamic filtering.
2. The Versatility of $fetch()
$fetch()
is a lightweight and versatile function that works both on the client and the server. However, unlike useFetch()
, it performs two requests (one on the server and one on the client) when used during SSR.
Ideal for Client-Side Interactions
Use $fetch()
for on-demand fetching triggered by user interactions, such as button clicks or form submissions:
<script setup>
function handleClick() {
$fetch("https://dummyjson.com/api/endpoint");
}
</script>
This makes $fetch()
perfect for fetching after page load, updating UI elements, or sending form data to APIs.
Working with Nuxt API Endpoints
Another powerful use case is interacting with your local API routes inside the server/api
directory:
<script setup>
const response = await $fetch("/api/user", {
method: "POST",
body: { name: "Jason" },
});
</script>
This method gives you a unified interface for both external and internal API requests, with built-in TypeScript support and automatic JSON parsing.
3. Harnessing the Flexibility of useAsyncData()
When your app doesn’t directly fetch from an HTTP endpoint, for example when working with Supabase, Firebase, or other SDKs, you can use useAsyncData()
.
Integrating SDKs and Libraries
const { data } = useAsyncData(async () => {
const { data, error } = await supabase.from("countries").select();
return data;
});
This composable is great for executing any async logic, not just API calls, and supports advanced use cases like parallel fetching and data transformation.
Parallel Fetching Made Simple
When you need multiple requests simultaneously, use Promise.all()
inside useAsyncData()
:
const { data } = await useAsyncData(() => {
return Promise.all([
$fetch("https://dummyjson.com/api/items/1"),
$fetch("https://dummyjson.com/api/reviews?item=1"),
]);
});
This significantly reduces total loading time by running all requests concurrently.
4. Advanced Caching Strategies
Caching in Nuxt 3 enhances performance by reducing redundant requests and serving preloaded data instantly.
Using key
and getCachedData
Both useFetch()
and useAsyncData()
allow specifying a key to cache and retrieve responses:
<script setup>
// with useFetch
const { data } = await useFetch("https://dummyjson.com/api/items", {
key: "items",
transform(data) {
return {
...data,
expiresAt: Date.now() + 10_000, // cache data for 10 seconds
};
},
getCachedData(key, nuxtApp) {
const data = nuxtApp.payload.data[key] || nuxtApp.static.data[key];
return data;
},
});
// with useAsyncData
const { data } = await useAsyncData("items", () => {
return $fetch("https://dummyjson.com/api/items");
}, {
transform(data) {
return {
...data,
expiresAt: Date.now() + 10_000, // cache data for 10 seconds
};
},
getCachedData(key, nuxtApp) {
const data = nuxtApp.payload.data[key] || nuxtApp.static.data[key];
return data;
},
});
</script>
This ensures data persists for a set time (e.g., 10 seconds here), improving speed and responsiveness. Please note that with useAsyncData()
, the first parameter is the key ("items"
).
5. Optimizing Data Handling with pick
and transform
Sometimes APIs return large datasets when you only need a small subset. The pick
option helps reduce payload size by selecting specific fields on the returned object data.
Picking Specific Fields
const { data } = await useFetch<{
firstName: string;
lastName: string;
}>("https://dummyjson.com/api/users/1", {
pick: ["firstName", "lastName"],
});
Although the full response is received from the server, only the picked fields are passed to the payload, slightly improving performance.
Transforming Lists
If the data retuened is a list, use the transform
option to restructure it efficiently:
const { data } = await useFetch<{
firstName: string;
lastName: string;
}[]>("https://dummyjson.com/api/users/", {
transform(data) {
return data.map((user) => ({
firstName: user.firstName,
lastName: user.lastName,
}));
},
});
This keeps your front-end clean and optimized without additional processing logic.
6. Handling Duplicate Requests with dedupe
When the same request is triggered multiple times, Nuxt provides deduplication control through the dedupe
option:
-
cancel
(default): Cancels any pending requests before starting a new one. -
defer
: Defers subsequent requests until the current one resolves.
<script setup>
const { data, execute } = await useFetch("https://dummyjson.com/api/endpoint", {
dedupe: "defer",
});
</script>
This prevents unnecessary API calls, saving bandwidth and avoiding race conditions.
7. Choosing the Right Method
Use Case | Recommended Method |
---|---|
Fetch data on initial page load | useFetch() |
Fetch on user interaction | $fetch() |
Work with SDKs or non-HTTP APIs | useAsyncData() |
Load data lazily or non-blocking | useLazyFetch() |
Perform multiple requests in parallel |
useAsyncData() + Promise.all()
|
Cache data between navigations |
useFetch() or useAsyncData() with key
|
Conclusion
Mastering data fetching in Nuxt 3 is fundamental to building responsive, SEO-friendly, and high-performance applications. By strategically combining useFetch()
, $fetch()
, and useAsyncData()
, along with options like lazy loading, deduplication, transform, and caching, developers can achieve seamless data flows, faster navigation, and superior UX.
Each method serves a unique purpose. Understanding when and how to use them is what separates a good Nuxt app from a great one.
Top comments (0)