Which composable is the right choice for fetching data, and when
useFetch is, at its core, a convenience wrapper around useAsyncData combined with $fetch. As soon as custom transform functions, precise caching keys, or a data source more complex than a plain HTTP request come into play, reaching for useAsyncData directly is usually the clearer choice.
Table of Contents
- 1. The Underlying Problem: Fetching Data in an SSR App
- 2. useFetch as a Convenience Wrapper
- 3. useAsyncData with a Custom Transform Function
- 4. Caching Keys in Detail
- 5. Decision Criteria: Which Composable, and When?
- 6. Common Pitfalls with Duplicate Data Fetching
- 7. Behavior Between Server and Client
- 8. The watch Option: Reacting to Reactive Parameters
- 9. Conclusion: Two Tools for the Same Purpose
- 10. Summary
- 11. FAQ
1. The Underlying Problem: Fetching Data in an SSR App
In a server-rendered Nuxt application, fetching data has to work correctly both on the server and in the browser, without the data being loaded twice. If you simply called a plain fetch inside an onMounted hook, there would be no data available on the server during the initial render, and the user would briefly see an empty state before the browser loads the data afterward.
Nuxt solves this problem through dedicated data composables that automatically synchronize the state of an asynchronous request between server and client. The server performs the fetch, serializes the result into the initial HTML payload, and the client picks up that result during hydration without executing the request a second time. This is exactly where both useFetch and useAsyncData come in.
2. useFetch as a Convenience Wrapper
useFetch(url, options) internally combines useAsyncData with $fetch and automatically generates a sensible caching key from the URL and the options passed in. For the most common case, namely fetching data directly from a known endpoint, this saves boilerplate, since neither a custom key nor a custom handler function has to be written.
The return values of useFetch match those of useAsyncData: data, pending, error, and refresh are all available as reactive values. In addition, useFetch supports the same options as $fetch directly, such as method, query, or headers, which makes defining a typical GET or POST request very compact.
3. useAsyncData with a Custom Transform Function
useAsyncData(key, handler, options) is the more flexible, general-purpose of the two composables. The handler is any asynchronous function, and it doesn't necessarily have to perform an HTTP request at all; it can combine several requests, read data from a database, or run a complex calculation. The key is specified explicitly and controls under which identifier the result gets cached in the Nuxt payload.
Especially useful is the transform option, which lets you shape the raw response into a more suitable form right after fetching, for example to filter out only the needed fields or to merge several endpoints into one combined result object. This keeps the stored payload smaller and lets the component work directly with the desired data structure.
// useFetch: compact for the standard case
const { data: product, pending, error } = await useFetch(
`/api/products/${id}`,
{ key: `product-${id}` }
);
// useAsyncData: full control over handler and transform
const { data: summary } = await useAsyncData(
`product-summary-${id}`,
async () => {
const [product, reviews] = await Promise.all([
$fetch(`/api/products/${id}`),
$fetch(`/api/products/${id}/reviews`),
]);
return { product, averageRating: reviews.average };
},
{
transform: (data) => ({
title: data.product.title,
rating: data.averageRating,
}),
}
);
4. Caching Keys in Detail
The caching key determines under which entry a result gets stored in the Nuxt payload, and it therefore also decides whether two calls at the same place in the code share the same cached result. With useFetch, the key is generated automatically from the URL, method, and body, which is unique enough in most cases, but doesn't always work optimally with dynamically generated URLs that carry complex query parameters.
With useAsyncData, the key has to be provided explicitly, which means a bit more typing up front, but also allows precise control. A common pattern is to compose the key from a combination of the route name and relevant parameters, such as `product-${id}-${locale}`, to make sure different language versions or variants don't accidentally share the same cached value.
5. Decision Criteria: Which Composable, and When?
For a simple, direct fetch from a single REST endpoint where the raw response can be used as-is, useFetch is usually the shorter and more readable choice. The automatically generated key and the direct pass-through of $fetch options reduce the code to its essentials without losing any functionality along the way.
As soon as several requests need to be combined, custom transform logic is needed, the data source isn't a plain HTTP call, or the caching key needs to be controlled manually for good reason, useAsyncData is the more fitting choice. As a rule of thumb: useFetch for the simple case, useAsyncData for everything beyond that.
6. Common Pitfalls with Duplicate Data Fetching
A very common mistake is loading data a second time on top of useFetch or useAsyncData via a direct $fetch call inside an onMounted hook. This causes the request to actually run twice, once on the server through the composable and once more in the browser, which creates unnecessary server load and a briefly inconsistent UI.
A second common pitfall involves missing or incorrectly set keys on dynamic routes: if you use the same, static key for a component that's reused across several detail pages with different IDs, Nuxt will incorrectly return the cached, stale data when navigating between two detail pages instead of triggering a new request. The key therefore always has to include the relevant, variable parameters.
7. Behavior Between Server and Client
By default, both composables perform the fetch during server-side rendering and transmit the result as part of the initial payload to the client. During hydration in the browser, Nuxt recognizes from the key that a result for this fetch already exists and doesn't run the request again, unless server and lazy have been explicitly configured otherwise.
With the server: false option, a fetch can be deliberately restricted to the client, which makes sense, for example, for data that needs browser-only information such as localStorage. The lazy: true option, in turn, doesn't delay rendering until the fetch result arrives; instead it shows the component immediately with pending: true and updates it once the data comes in.
8. The watch Option: Reacting to Reactive Parameters
When a fetch depends on a reactive value, such as a sort order held in a ref or a search term from an input field, the request should ideally re-run automatically whenever that value changes. Both composables support the watch option for this, which takes a list of reactive sources whose changes trigger a fresh fetch.
Without an explicitly set watch option, neither useFetch nor useAsyncData automatically reacts to changes in variables used inside the handler or the URL, even if those variables are reactive. Overlooking this connection leaves a list incorrectly stuck in its old state after a search term changes, until the page is manually reloaded, which in practice is one of the most confusing failure modes with these composables.
9. Conclusion: Two Tools for the Same Purpose
useFetch and useAsyncData solve the same fundamental problem of synchronized data fetching between server and client, but differ noticeably in the level of control they offer. Anyone who makes the choice deliberately based on the actual complexity of the use case, rather than reflexively always reaching for the same composable, tends to write shorter and, at the same time, more correct code.
What remains especially important in both cases is a well-thought-out caching key and deliberately avoiding extra, manual fetch calls outside the composables. Anyone who observes these two basic rules reliably avoids the most common performance and consistency problems with data fetching in Nuxt applications.
| Aspect | useFetch | useAsyncData |
|---|---|---|
| Caching key | Generated automatically from URL/options | Must be provided explicitly |
| Data source | Direct $fetch request | Any asynchronous handler function |
| Transform option | Available via options object | Available with full control in the handler |
| Code footprint | Compact for standard cases | Slightly more code, more flexibility |
| Typical use | Single REST endpoint | Combined requests, complex logic |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
useFetch vs. useAsyncData: The Essentials at a Glance
useFetch
Convenience wrapper around useAsyncData plus $fetch for the standard case
useAsyncData
Full control over the handler, transform function, and caching key
Most common mistake
An extra manual fetch in onMounted causes a duplicate request
Rule of thumb
useFetch for simple endpoints, useAsyncData for anything more complex