docs: rewrite data fetching section (#21031)

This commit is contained in:
Clément Ollivier 2023-05-25 23:38:55 +02:00 committed by GitHub
parent 75cd6b7894
commit 200bc025c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -3,24 +3,45 @@ navigation.icon: uil:channel
description: Nuxt provides composables to handle data fetching within your application.
---
# Data Fetching
# Data fetching
Nuxt provides useFetch, useLazyFetch, useAsyncData and useLazyAsyncData to handle data fetching within your application.
Nuxt comes with two composables and a built-in library to perform data-fetching in browser or server environments: `useFetch`, `useAsyncData` and `$fetch` .
Used together, they ensure cross-environment compatibility and efficient caching and avoid duplicate network calls.
`useFetch` is the most straightforward way to perform API calls in Nuxt.
If you need more fine-grained control, you can use `useAsyncData` and `$fetch` independently.
The two composables share a common set of options and patterns that we will detail in the last sections.
## Why using specific composables?
When using a framework like Nuxt that can perform calls and render pages on both client and server environments, some challenges must be addressed. This is why Nuxt provides composables to wrap your queries.
### Network calls duplication
The `useFetch` and `useAsyncData` composables ensure that once an API call is made on the server, the data is properly forwarded to the client in the payload. This JavaScript object is accessible through `window.__NUXT__` and is used on the client to avoid refetching the same data when the code is executed in the browser.
::alert{icon=⚙️}
Use the [Nuxt DevTools](https://devtools.nuxtjs.org) to inspect this data in the payload tab.
::
### Effective caching
`useFetch` and `useAsyncData` both use a key to cache API responses and further reduce API calls. We will detail later how to invalidate this cache.
### Suspense
Nuxt uses Vues `<Suspense>` component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-calls basis.
::alert{icon=👉}
**`useFetch`, `useLazyFetch`, `useAsyncData` and `useLazyAsyncData` only work during `setup` or `Lifecycle Hooks`**
These composables are auto-imported and can be used in `setup` functions or lifecycle hooks
::
## `useFetch`
Within your pages, components and plugins you can use `useFetch` to universally fetch from any URL.
This composable provides a convenient wrapper around `useAsyncData` and `$fetch`. It automatically generates a key based on URL and fetch options, provides type hints for request url based on server routes, and infers API response type.
::ReadMore{link="/docs/api/composables/use-fetch"}
::
### Example
`useFetch` is the most straightforward way to perform data fetching. It is a wrapper around the `useAsyncData` composable and `$fetch` utility.
```vue [app.vue]
<script setup>
@ -32,19 +53,67 @@ const { data: count } = await useFetch('/api/count')
</template>
```
::ReadMore{link="/docs/api/composables/use-fetch"}
::
::LinkExample{link="/docs/examples/composables/use-fetch"}
::
## `useLazyFetch`
## `$fetch`
This composable behaves identically to `useFetch` with the `lazy: true` option set. In other words, the async function does not block navigation. That means you will need to handle the situation where the data is `null` (or whatever value you have provided in a custom `default` factory function).
The `ofetch` library is built on top of the `fetch` API and adds handy features to it:
::ReadMore{link="/docs/api/composables/use-lazy-fetch"}
- Works the same way in browser, Node or worker environments
- Automatic response parsing
- Error handling
- Auto-retry
- Interceptors
::alert{icon=📘}
[Read the full documentation of ofetch](https://github.com/unjs/ofetch)
::
### Example
ofetch is auto-imported by Nuxt and used by the `useFetch` composable.
```vue
It can also be used in your whole application with the `$fetch` alias:
```ts
const users = await $fetch('/api/users').catch((error) => error.data)
```
::alert{type="warning"}
Beware that using only `$fetch` will not provide the benefits described in [the first section of this page](#why-using-specific-composables). It is recommended to use `$fetch` when doing actions purely on client-side or combined with `useAsyncData`.
::
::ReadMore{link="/docs/api/utils/dollarfetch"}
::
## `useAsyncData`
`useFetch` receives a URL and gets that data, whereas `useAsyncData` might have more complex logic. `useFetch(url)` is nearly equivalent to `useAsyncData(url, () => $fetch(url))` - it's developer experience sugar for the most common use case.
There are some cases when using the `useFetch` composable is not appropriate, for example when a CMS or a third-party provide their own query layer. In this case, you can use `useAsyncData` to wrap your calls and still keep the benefits provided by the composable:
```ts
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
```
::alert{icon=👉}
The first argument of `useAsyncData` is the unique key used to cache the response of the second argument, the querying function. This argument can be ignored by directly passing the querying function. In that case, it will be auto-generated.
::
::ReadMore{link="/docs/api/composables/use-async-data"}
::
## Options
`useAsyncData` and `useFetch` return the same object type and accept a common set of options as their last argument. They can help you control the composables behavior, such as navigation blocking, caching or execution.
### Lazy
By default, data fetching composables will wait for the resolution of their asynchronous function before navigating to a new page by using Vues Suspense. This feature can be ignored on client-side navigation with the `lazy` option. In that case, you will have to manually handle loading state using the `pending` value.
```vue [app.vue]
<template>
<!-- you will need to handle a loading state -->
<div v-if="pending">
@ -56,238 +125,111 @@ This composable behaves identically to `useFetch` with the `lazy: true` option s
</div>
</div>
</template>
<script setup>
const { pending, data: posts } = await useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
// Because posts might start out null, you will not have access
// to its contents immediately, but you can watch it.
const { pending, data: posts } = useFetch('/api/posts', {
lazy: true
})
</script>
```
## `useAsyncData`
You can alternatively use `useLazyFetch` and `useLazyAsyncData` as convenient methods to perform the same.
Within your pages, components and plugins you can use `useAsyncData` to get access to data that resolves asynchronously.
::alert
You might be asking yourself: what is the difference between `useFetch` and `useAsyncData`?
In brief, `useFetch` receives a URL and gets that data, whereas `useAsyncData` might have more complex logic. `useFetch(url)` is nearly equivalent to `useAsyncData(url, () => $fetch(url))` - it's developer experience sugar for the most common use case.
::
::ReadMore{link="/docs/api/composables/use-async-data"}
::
### Example
```ts [server/api/count.ts]
let counter = 0
export default defineEventHandler(() => {
counter++
return counter
})
```ts
const { pending, data: posts } = useLazyFetch('/api/posts')
```
```vue [app.vue]
<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script>
<template>
Page visits: {{ data }}
</template>
```
::LinkExample{link="/docs/examples/composables/use-async-data"}
::ReadMore{link="/docs/api/composables/use-lazy-fetch"}
::
## `useLazyAsyncData`
This composable behaves identically to `useAsyncData` with the `lazy: true` option set. In other words, the async function does not block navigation. That means you will need to handle the situation where the data is `null` (or whatever value you have provided in a custom `default` factory function).
::ReadMore{link="/docs/api/composables/use-lazy-async-data"}
::
### Example
### Client-only fetching
```vue
<template>
<div>
{{ pending ? 'Loading' : count }}
</div>
</template>
By default, data fetching composables will perform their asynchronous function on both client and server environments. Set the `server` option to `false` to only perform the call on the client-side. Combined with the `lazy` option, this can be useful for data that are not needed on the first render (for example, non-SEO sensitive data).
<script setup>
const { pending, data: count } = await useLazyAsyncData('count', () => $fetch('/api/count'))
watch(count, (newCount) => {
// Because count starts out null, you won't have access
// to its contents immediately, but you can watch it.
```ts
/* This call will only be performed on the client */
const { pending, data: posts } = useFetch('/api/comments', {
lazy: true,
server: false
})
</script>
```
## Transforming Data
### Minimize payload size
If for some reason you are not satisfied with how the requested data is structured, you may want to use the `transform` option from `useFetch` or `useAsyncData` to alter the data by using a function to perform the necessary transformation after your request has been resolved.
We very much recommend that you remove any data or properties that you do not need, in order to reduce your page's payload size - see the [Minimize Payload](#minimize-payload) section.
### Example
The `pick` option helps you to minimize the payload size stored in your HTML document by only selecting the fields that you want returned from the composables.
```vue
<script setup>
const { data: users } = await useAsyncData(
"users",
() => $fetch('/api/users'),
{
transform: (users) =>
users.map((user) => ({
id: user.id,
fullName: `${user.firstName} ${user.surname}`,
})),
}
);
/* only pick the fields used in your template */
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
</script>
<template>
<div>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.fullName }}
</li>
</ul>
</div>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>
```
If you are dealing with a complex data structure and you are only interested in a few properties, simply make use of the `pick` option to only pick specific properties you are interested in.
If you need more control or map over several objects, you can use the `transform` function to alter the result of the query.
```vue
<script setup>
const { data: user } = await useFetch('/api/users/123', {
pick: ["firstName", "surname"],
});
const userFullName = computed(
() => `${user.value.firstName} ${user.value.surname}`
);
</script>
<template>
<div>Hi {{ userFullName }}</div>
</template>
```
## Refreshing Data
Sometimes throughout the course of your user's page visit, you may need to refresh the data loaded from the API. This can happen if the user chooses to paginate, filter results, search, etc.
You can make use of the `refresh()` method returned from the `useFetch()` composable to refresh the data with different query parameters:
```vue
<script setup>
const page = ref(1)
const { data: users, pending, refresh, error } = await useFetch(() => `users?page=${page.value}&take=6`, { baseURL: config.API_BASE_URL }
)
function previous() {
page.value--
refresh()
}
function next() {
page.value++
refresh()
}
</script>
```
The key to making this work is to call the `refresh()` method returned from the `useFetch()` composable when a query parameter has changed.
By default, `refresh()` will cancel any pending requests their result will not update the data or pending state. Any previously awaited promises will not resolve until this new request resolves. You can prevent this behaviour by setting the `dedupe` option, which will instead return the promise for the currently-executing request, if there is one.
```js
refresh({ dedupe: true })
```
### `refreshNuxtData`
Invalidate the cache of `useAsyncData`, `useLazyAsyncData`, `useFetch` and `useLazyFetch` and trigger the refetch.
This method is useful if you want to refresh all the data fetching for a current page.
::ReadMore{link="/docs/api/utils/refresh-nuxt-data"}
::
#### Example
```vue
<template>
<div>
{{ pending ? 'Loading' : count }}
</div>
<button @click="refresh">Refresh</button>
</template>
<script setup>
const { pending, data: count } = await useLazyAsyncData('count', () => $fetch('/api/count'))
const refresh = () => refreshNuxtData('count')
</script>
```
### `clearNuxtData`
Delete cached data, error status and pending promises of `useAsyncData` and `useFetch`.
This method is useful if you want to invalidate the data fetching for another page.
::ReadMore{link="/docs/api/utils/clear-nuxt-data"}
::
## Options API support
Nuxt 3 provides a way to perform `asyncData` fetching within the Options API. You must wrap your component definition within `defineNuxtComponent` for this to work.
```vue
<script>
export default defineNuxtComponent({
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello')
}
```ts
const { data: mountains } = await useFetch('/api/mountains', {
transform: (mountains) => {
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
}
})
</script>
```
::Alert
Using `<script setup lang="ts">` is the recommended way of declaring Vue components in Nuxt 3.
### Caching and refetching
#### Keys
`useFetch` and `useAsyncData` use keys to prevent refetching the same data (for example when navigating back to a page previously rendered).
- `useFetch` uses the provided URL as a key. Alternatively, a `key` value can be provided in the `options` object passed as a last argument.
- `useAsyncData` uses its first argument as a key if it is a string. If the first argument is the handler function that performs the query, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you.
#### Refresh
If you want to force the function to re-run, you can manually change the key or use the `refresh` function provided by the composables.
```vue
<script setup>
const { data, error, refresh } = await useFetch('/api/users')
</script>
<template>
<div>
<p>{{ data }}</p>
<button @click="refresh">Refresh data</button>
</div>
</template>
```
::alert{icon=📘}
To globally refetch or invalidate cached data, see [`clearNuxtData`](/docs/api/utils/clear-nuxt-data) and [`refreshNuxtData`](/docs/api/utils/refresh-nuxt-data).
::
::ReadMore{link="/docs/api/utils/define-nuxt-component"}
::
#### Watch
## Using `$fetch` directly
To re-run your fetching function each time other reactive values in your application change, use the `watch` option.
There are instances where you may need to directly call the API. Nuxt 3 provides a globally available `$fetch` method using [unjs/ofetch](https://github.com/unjs/ofetch) (in addition to `fetch`) with the same API as the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch).
```ts
const { data, error, refresh } = await useFetch('/api/users', {
/* Changing the id will trigger a refetch */
watch: [id]
})
Using `$fetch` has a number of benefits, including:
const id = ref(1)
```
It will handle 'smartly' making direct API calls if it's running on the server, or making a client-side call to your API if it's running on the client. (It can also handle calling third-party APIs.)
Plus, it comes with convenience features including automatically parsing responses and stringifying data.
::ReadMore{link="/docs/api/utils/dollarfetch"}
::
### Isomorphic `$fetch` and `fetch` calls
## Passing Headers and cookies
When we call `$fetch` in the browser, user headers like `cookie` will be directly sent to the API. But during server-side-rendering, since the `$fetch` request takes place 'internally' within the server, it doesn't include the user's browser cookies, nor does it pass on cookies from the fetch response.
#### Example: Pass Client Headers to the API
### Pass Client Headers to the API
We can use [`useRequestHeaders`](/docs/api/composables/use-request-headers) to access and proxy cookies to the API from server-side.
@ -303,13 +245,13 @@ const { data } = await useFetch('/api/me', { headers })
::alert{type="warning"}
Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied:
* `host`, `accept`
* `content-length`, `content-md5`, `content-type`
* `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-proto`
* `cf-connecting-ip`, `cf-ray`
- `host`, `accept`
- `content-length`, `content-md5`, `content-type`
- `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-proto`
- `cf-connecting-ip`, `cf-ray`
::
#### Example: Pass Cookies From Server-side API Calls on SSR Response
### Pass Cookies From Server-side API Calls on SSR Response
If you want to pass on/proxy cookies in the other direction, from an internal request back to the client, you will need to handle this yourself.
@ -335,111 +277,31 @@ onMounted(() => console.log(document.cookie))
</script>
```
## Best Practices
## Options API support
### Minimize Payload
The data returned by these composables will be stored inside the page payload. This means that every key returned that is not used in your component will be added to the payload.
::alert{icon=👉}
**We strongly recommend you only select the keys that you will use in your component.**
::
Imagine that `/api/mountains/everest` returns the following object:
```json
{
"title": "Mount Everest",
"description": "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The ChinaNepal border runs across its summit point",
"height": "8,848 m",
"countries": [
"China",
"Nepal"
],
"continent": "Asia",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg"
}
```
If you plan to only use `title` and `description` in your component, you can select the keys by chaining the result of `$fetch` or `pick` option:
```vue
<script setup>
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
</script>
<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>
```
### Avoid double calls
Calling `$fetch` in code that is executed on both server and client (such as in the top level of a `setup` function) will fetch the data twice - initially on the server and then again on the client during the hydration phase. This is because `$fetch` does not automatically serialize or transfer the data to the client.
For example:
**/pages/price.vue**: Isomorphic code below executes `$fetch` twice (initially on the server, then again on the client).
```ts
<script setup lang="ts">
const price = $fetch('/api/price');
</script>
```
**/server/api/product.get.ts**: Server only code below executes `$fetch` only once at the server side.
```ts
export default eventHandler(async (event: H3Event) => {
const price = $fetch('/api/price');
return { color: getColor(), price };
});
```
If fetching twice isn't your intended behavior, to fetch only on the server side and transfer it to the client, wrap `$fetch` with `useAsyncData()` or use `useFetch()`.
```ts
<script setup lang="ts">
const { data } = await useAsyncData('price', () => $fetch('/api/price'));
// or
const { data } = await useFetch('/api/price')
</script>
```
## Using Async Setup
If you are using `async setup()`, the current component instance will be lost after the first `await`. (This is a Vue 3 limitation.) If you want to use multiple async operations, such as multiple calls to `useFetch`, you will need to use `<script setup>`, await them together at the end of setup, or alternatively use `defineNuxtComponent` (which applies a custom transform to the setup function).
::alert{icon=👉}
Using `<script setup>` is recommended, as it removes the limitation of using top-level await. [Read more](https://vuejs.org/api/sfc-script-setup.html#top-level-await)
::
Nuxt 3 provides a way to perform `asyncData` fetching within the Options API. You must wrap your component definition within `defineNuxtComponent` for this to work.
```vue
<script>
export default defineComponent({
async setup() {
const [{ data: organization }, { data: repos }] = await Promise.all([
useFetch(`https://api.github.com/orgs/nuxt`),
useFetch(`https://api.github.com/orgs/nuxt/repos`)
])
export default defineNuxtComponent({
/* Use the fetchKey option to provide a unique key */
fetchKey: 'hello',
async asyncData () {
return {
organization,
repos
hello: await $fetch('/api/hello')
}
}
})
</script>
<template>
<header>
<h1>{{ organization.login }}</h1>
<p>{{ organization.description }}</p>
</header>
</template>
```
::Alert
Using `<script setup lang="ts">` is the recommended way of declaring Vue components in Nuxt 3.
::
::ReadMore{link="/docs/api/utils/define-nuxt-component"}
::
## Serialization
When fetching data from the `server` directory, the response is serialized using `JSON.stringify`. However, since serialization is limited to only JavaScript primitive types, Nuxt does its best to convert the return type of `$fetch` and `useFetch` to match the actual value.