mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-24 14:45:15 +00:00
540 lines
19 KiB
Markdown
540 lines
19 KiB
Markdown
---
|
||
title: 'Data fetching'
|
||
description: Nuxt provides composables to handle data fetching within your application.
|
||
navigation.icon: i-ph-plugs-connected-duotone
|
||
---
|
||
|
||
Nuxt comes with two composables and a built-in library to perform data-fetching in browser or server environments: `useFetch`, [`useAsyncData`](/docs/api/composables/use-async-data) and `$fetch`.
|
||
|
||
In a nutshell:
|
||
|
||
- [`useFetch`](/docs/api/composables/use-fetch) is the most straightforward way to handle data fetching in a component setup function.
|
||
- [`$fetch`](/docs/api/utils/dollarfetch) is great to make network requests based on user interaction.
|
||
- [`useAsyncData`](/docs/api/composables/use-async-data), combined with `$fetch`, offers more fine-grained control.
|
||
|
||
Both `useFetch` and `useAsyncData` share a common set of options and patterns that we will detail in the last sections.
|
||
|
||
Before that, it's imperative to know why these composables exist in the first place.
|
||
|
||
## 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, instead of letting the developer rely on [`$fetch`](/docs/api/utils/dollarfetch) calls alone.
|
||
|
||
### Network calls duplication
|
||
|
||
The [`useFetch`](/docs/api/composables/use-fetch) and [`useAsyncData`](/docs/api/composables/use-async-data) composables ensure that once an API call is made on the server, the data is properly forwarded to the client in the payload.
|
||
|
||
The payload is a JavaScript object accessible through [`useNuxtApp().payload`](/docs/api/composables/use-nuxt-app#payload). It is used on the client to avoid refetching the same data when the code is executed in the browser.
|
||
|
||
::callout
|
||
Use the [Nuxt DevTools](https://devtools.nuxt.com) to inspect this data in the **Payload tab**.
|
||
::
|
||
|
||
### Suspense
|
||
|
||
Nuxt uses Vue’s [`<Suspense>`](https://vuejs.org/guide/built-ins/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.
|
||
|
||
## `useFetch`
|
||
|
||
The [`useFetch`](/docs/api/composables/use-fetch) composable is the most straightforward way to perform data fetching.
|
||
|
||
```vue [app.vue]
|
||
<script setup lang="ts">
|
||
const { data: count } = await useFetch('/api/count')
|
||
</script>
|
||
|
||
<template>
|
||
Page visits: {{ count }}
|
||
</template>
|
||
```
|
||
|
||
This composable is a wrapper around the [`useAsyncData`](/docs/api/composables/use-async-data) composable and `$fetch` utility.
|
||
|
||
:read-more{to="/docs/api/composables/use-fetch"}
|
||
|
||
:link-example{to="/docs/examples/features/data-fetching"}
|
||
|
||
## `$fetch`
|
||
|
||
Nuxt includes the `ofetch` library, and is auto-imported as the `$fetch` alias globally across your application. It's what `useFetch` uses behind the scenes.
|
||
|
||
```ts
|
||
const users = await $fetch('/api/users').catch((error) => error.data)
|
||
```
|
||
|
||
::callout
|
||
Beware that using only `$fetch` will not provide [network calls de-duplication and navigation prevention](#why-using-specific-composables). It is recommended to use `$fetch` when posting data to an event handler, when doing client-side only logic, or combined with `useAsyncData`.
|
||
::
|
||
|
||
The `ofetch` library is built on top of [the `fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) and adds handy features to it:
|
||
|
||
- Works the same way in browser, Node or worker environments
|
||
- Automatic response parsing
|
||
- Error handling
|
||
- Auto-retry
|
||
- Interceptors
|
||
|
||
::read-more{title="ofetch" to="https://github.com/unjs/ofetch" target="_blank"}
|
||
Read the full documentation of `ofetch`
|
||
::
|
||
|
||
::read-more{to="/docs/api/utils/dollarfetch"}
|
||
Read more about `$fetch`
|
||
::
|
||
|
||
## `useAsyncData`
|
||
|
||
The `useAsyncData` composable is responsible for wrapping async logic and returning the result once it is resolved.
|
||
|
||
Indeed, `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`](/docs/api/composables/use-fetch) 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`](/docs/api/composables/use-async-data) to wrap your calls and still keep the benefits provided by the composable.
|
||
|
||
The first argument of [`useAsyncData`](/docs/api/composables/use-async-data) 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.
|
||
|
||
```ts
|
||
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
|
||
```
|
||
|
||
Since the autogenerated key only takes into account the file and line where `useAsyncData` is invoked, it is recommended to always create your own key to avoid unwanted behavior, if you are creating your own custom composable that is wrapping `useAsyncData`.
|
||
|
||
```ts
|
||
const id = ref(1)
|
||
|
||
const { data, error } = await useAsyncData(`user:${id.value}`, () => {
|
||
return myGetFunction('users', { id: id.value })
|
||
})
|
||
```
|
||
|
||
The `useAsyncData` composable is a great way to wrap and wait for multiple `useFetch` to be done, and then retrieve the results of each.
|
||
|
||
```ts
|
||
const { data: discounts, pending } = await useAsyncData('cart-discount', async () => {
|
||
const [coupons, offers] = await Promise.all([$fetch('/cart/coupons'), $fetch('/cart/offers')])
|
||
|
||
return {
|
||
coupons,
|
||
offers
|
||
}
|
||
})
|
||
```
|
||
|
||
::read-more{to="/docs/api/composables/use-async-data"}
|
||
Read more about `useAsyncData`
|
||
::
|
||
|
||
## Options
|
||
|
||
[`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) 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 Vue’s 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]
|
||
<script setup lang="ts">
|
||
const { pending, data: posts } = useFetch('/api/posts', {
|
||
lazy: true
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<!-- you will need to handle a loading state -->
|
||
<div v-if="pending">
|
||
Loading ...
|
||
</div>
|
||
<div v-else>
|
||
<div v-for="post in posts">
|
||
<!-- do something -->
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
You can alternatively use [`useLazyFetch`](/docs/api/composables/use-lazy-fetch) and `useLazyAsyncData` as convenient methods to perform the same.
|
||
|
||
```ts
|
||
const { pending, data: posts } = useLazyFetch('/api/posts')
|
||
```
|
||
|
||
::read-more{to="/docs/api/composables/use-lazy-fetch"}
|
||
Read more about `useLazyFetch`
|
||
::
|
||
|
||
::read-more{to="/docs/api/composables/use-lazy-async-data"}
|
||
Read more about `useLazyAsyncData`
|
||
::
|
||
|
||
### Client-only fetching
|
||
|
||
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. On initial load, the data will not be fetched before hydration is complete so you have to handle a pending state, though on subsequent client-side navigation the data will be awaited before loading the page.
|
||
|
||
Combined with the `lazy` option, this can be useful for data that is not needed on the first render (for example, non-SEO sensitive data).
|
||
|
||
```ts
|
||
/* This call is performed before hydration */
|
||
const { article } = await useFetch('api/article')
|
||
|
||
/* This call will only be performed on the client */
|
||
const { pending, data: posts } = useFetch('/api/comments', {
|
||
lazy: true,
|
||
server: false
|
||
})
|
||
```
|
||
|
||
The `useFetch` composable is meant to be invoked in setup method or called directly at the top level of a function in lifecycle hooks, otherwise you should use [`$fetch` method](#fetch).
|
||
|
||
### Minimize payload size
|
||
|
||
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 lang="ts">
|
||
/* only pick the fields used in your template */
|
||
const { data: mountain } = await useFetch('/api/mountains/everest', { pick: ['title', 'description'] })
|
||
</script>
|
||
|
||
<template>
|
||
<h1>{{ mountain.title }}</h1>
|
||
<p>{{ mountain.description }}</p>
|
||
</template>
|
||
```
|
||
|
||
If you need more control or map over several objects, you can use the `transform` function to alter the result of the query.
|
||
|
||
```ts
|
||
const { data: mountains } = await useFetch('/api/mountains', {
|
||
transform: (mountains) => {
|
||
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
|
||
}
|
||
})
|
||
```
|
||
|
||
::callout
|
||
Both `pick` and `transform` don't prevent the unwanted data from being fetched initially. But they will prevent unwanted data from being added to the payload transferred from server to client.
|
||
::
|
||
|
||
### Caching and refetching
|
||
|
||
#### Keys
|
||
|
||
[`useFetch`](/docs/api/composables/use-fetch) and [`useAsyncData`](/docs/api/composables/use-async-data) use keys to prevent refetching the same data.
|
||
|
||
- [`useFetch`](/docs/api/composables/use-fetch) uses the provided URL as a key. Alternatively, a `key` value can be provided in the `options` object passed as a last argument.
|
||
- [`useAsyncData`](/docs/api/composables/use-async-data) 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.
|
||
|
||
::callout
|
||
To get the cached data by key, you can use [`useNuxtData`](/docs/api/composables/use-nuxt-data)
|
||
::
|
||
|
||
#### Refresh and execute
|
||
|
||
If you want to fetch or refresh data manually, use the `execute` or `refresh` function provided by the composables.
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
const { data, error, execute, refresh } = await useFetch('/api/users')
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<p>{{ data }}</p>
|
||
<button @click="refresh">Refresh data</button>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
The `execute` function is an alias for `refresh` that works in exactly the same way but is more semantic for cases when the fetch is [not immediate](#not-immediate).
|
||
|
||
::callout
|
||
To globally refetch or invalidate cached data, see [`clearNuxtData`](/docs/api/utils/clear-nuxt-data) and [`refreshNuxtData`](/docs/api/utils/refresh-nuxt-data).
|
||
::
|
||
|
||
#### Watch
|
||
|
||
To re-run your fetching function each time other reactive values in your application change, use the `watch` option. You can use it for one or multiple _watchable_ elements.
|
||
|
||
```ts
|
||
const id = ref(1)
|
||
|
||
const { data, error, refresh } = await useFetch('/api/users', {
|
||
/* Changing the id will trigger a refetch */
|
||
watch: [id]
|
||
})
|
||
```
|
||
|
||
Note that **watching a reactive value won't change the URL fetched**. For example, this will keep fetching the same initial ID of the user because the URL is constructed at the moment the function is invoked.
|
||
|
||
```ts
|
||
const id = ref(1)
|
||
|
||
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
|
||
watch: [id]
|
||
})
|
||
```
|
||
|
||
If you need to change the URL based on a reactive value, you may want to use a [computed URL](#computed-url) instead.
|
||
|
||
#### Computed URL
|
||
|
||
Sometimes you may need to compute an URL from reactive values, and refresh the data each time these change. Instead of juggling your way around, you can attach each param as a reactive value. Nuxt will automatically use the reactive value and re-fetch each time it changes.
|
||
|
||
```ts
|
||
const id = ref(null)
|
||
|
||
const { data, pending } = useLazyFetch('/api/user', {
|
||
query: {
|
||
user_id: id
|
||
}
|
||
})
|
||
```
|
||
|
||
In the case of more complex URL construction, you may use a callback as a [computed getter](https://vuejs.org/guide/essentials/computed.html) that returns the URL string.
|
||
|
||
Every time a dependency changes, the data will be fetched using the newly constructed URL. Combine this with [not-immediate](#not-immediate), and you can wait until the reactive element changes before fetching.
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
const id = ref(null)
|
||
|
||
const { data, pending, status } = useLazyFetch(() => `/api/users/${id.value}`, {
|
||
immediate: false
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<!-- disable the input while fetching -->
|
||
<input v-model="id" type="number" :disabled="pending"/>
|
||
|
||
<div v-if="status === 'idle'">
|
||
Type an user ID
|
||
</div>
|
||
|
||
<div v-else-if="pending">
|
||
Loading ...
|
||
</div>
|
||
|
||
<div v-else>
|
||
{{ data }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
If you need to force a refresh when other reactive values change, you can also [watch other values](#watch).
|
||
|
||
### Not immediate
|
||
|
||
The `useFetch` composable will start fetching data the moment is invoked. You may prevent this by setting `immediate: false`, for example, to wait for user interaction.
|
||
|
||
With that, you will need both the `status` to handle the fetch lifecycle, and `execute` to start the data fetch.
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
const { data, error, execute, pending, status } = await useLazyFetch('/api/comments')
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="status === 'idle'">
|
||
<button @click="execute">Get data</button>
|
||
</div>
|
||
|
||
<div v-else-if="pending">
|
||
Loading comments...
|
||
</div>
|
||
|
||
<div v-else>
|
||
{{ data }}
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
For finer control, the `status` variable can be:
|
||
|
||
- `idle` when the fetch hasn't started
|
||
- `pending` when a fetch has started but not yet completed
|
||
- `error` when the fetch fails
|
||
- `success` when the fetch is completed successfully
|
||
|
||
## 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.
|
||
|
||
### 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.
|
||
|
||
The example below adds the request headers to an isomorphic `$fetch` call to ensure that the API endpoint has access to the same `cookie` header originally sent by the user.
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
const headers = useRequestHeaders(['cookie'])
|
||
|
||
const { data } = await useFetch('/api/me', { headers })
|
||
</script>
|
||
```
|
||
|
||
::callout
|
||
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`
|
||
::
|
||
|
||
### 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.
|
||
|
||
```ts [composables/fetch.ts]
|
||
import { appendResponseHeader, H3Event } from 'h3'
|
||
|
||
export const fetchWithCookie = async (event: H3Event, url: string) => {
|
||
/* Get the response from the server endpoint */
|
||
const res = await $fetch.raw(url)
|
||
/* Get the cookies from the response */
|
||
const cookies = (res.headers.get('set-cookie') || '').split(',')
|
||
/* Attach each cookie to our incoming Request */
|
||
for (const cookie of cookies) {
|
||
appendResponseHeader(event, 'set-cookie', cookie)
|
||
}
|
||
/* Return the data of the response */
|
||
return res._data
|
||
}
|
||
```
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
// This composable will automatically pass cookies to the client
|
||
const event = useRequestEvent()
|
||
|
||
const result = await fetchWithCookie(event, '/api/with-cookie')
|
||
|
||
onMounted(() => console.log(document.cookie))
|
||
</script>
|
||
```
|
||
|
||
## 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({
|
||
/* Use the fetchKey option to provide a unique key */
|
||
fetchKey: 'hello',
|
||
async asyncData () {
|
||
return {
|
||
hello: await $fetch('/api/hello')
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
```
|
||
|
||
::callout
|
||
Using `<script setup lang="ts">` is the recommended way of declaring Vue components in Nuxt 3.
|
||
::
|
||
|
||
:read-more{to="/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`](/docs/api/composables/use-fetch) to match the actual value.
|
||
|
||
::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description" target="_blank"}
|
||
Learn more about `JSON.stringify` limitations.
|
||
::
|
||
|
||
### Example
|
||
|
||
```ts [server/api/foo.ts]
|
||
export default defineEventHandler(() => {
|
||
return new Date()
|
||
})
|
||
```
|
||
|
||
```vue [app.vue]
|
||
<script setup lang="ts">
|
||
// Type of `data` is inferred as string even though we returned a Date object
|
||
const { data } = await useFetch('/api/foo')
|
||
</script>
|
||
```
|
||
|
||
### Custom serializer function
|
||
|
||
To customize the serialization behavior, you can define a `toJSON` function on your returned object. If you define a `toJSON` method, Nuxt will respect the return type of the function and will not try to convert the types.
|
||
|
||
```ts [server/api/bar.ts]
|
||
export default defineEventHandler(() => {
|
||
const data = {
|
||
createdAt: new Date(),
|
||
|
||
toJSON() {
|
||
return {
|
||
createdAt: {
|
||
year: this.createdAt.getFullYear(),
|
||
month: this.createdAt.getMonth(),
|
||
day: this.createdAt.getDate(),
|
||
},
|
||
}
|
||
},
|
||
}
|
||
return data
|
||
})
|
||
|
||
```
|
||
|
||
```vue [app.vue]
|
||
<script setup lang="ts">
|
||
// Type of `data` is inferred as
|
||
// {
|
||
// createdAt: {
|
||
// year: number
|
||
// month: number
|
||
// day: number
|
||
// }
|
||
// }
|
||
const { data } = await useFetch('/api/bar')
|
||
</script>
|
||
```
|
||
|
||
### Using an alternative serializer
|
||
|
||
Nuxt does not currently support an alternative serializer to `JSON.stringify`. However, you can return your payload as a normal string and utilize the `toJSON` method to maintain type safety.
|
||
|
||
In the example below, we use [superjson](https://github.com/blitz-js/superjson) as our serializer.
|
||
|
||
```ts [server/api/superjson.ts]
|
||
import superjson from 'superjson'
|
||
|
||
export default defineEventHandler(() => {
|
||
const data = {
|
||
createdAt: new Date(),
|
||
|
||
// Workaround the type conversion
|
||
toJSON() {
|
||
return this
|
||
}
|
||
}
|
||
|
||
// Serialize the output to string, using superjson
|
||
return superjson.stringify(data) as unknown as typeof data
|
||
})
|
||
```
|
||
|
||
```vue [app.vue]
|
||
<script setup lang="ts">
|
||
import superjson from 'superjson'
|
||
|
||
// `date` is inferred as { createdAt: Date } and you can safely use the Date object methods
|
||
const { data } = await useFetch('/api/superjson', {
|
||
transform: (value) => {
|
||
return superjson.parse(value as unknown as string)
|
||
},
|
||
})
|
||
</script>
|
||
```
|