Data fetching
Nuxt comes with two composables and a built-in library to perform data-fetching in browser or server environments: useFetch
, useAsyncData
and $fetch
.
In a nutshell:
useFetch
is the most straightforward way to handle data fetching in a component setup function.$fetch
is great to make network requests based on user interaction.useAsyncData
, 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 use specific composables for data fetching?
Nuxt is a framework which can run isomorphic (or universal) code in both server and client environments. If the $fetch
function is used to perform data fetching in the setup function of a Vue component, this may cause data to be fetched twice, once on the server (to render the HTML) and once again on the client (when the HTML is hydrated). This is why Nuxt offers specific data fetching composables so data is fetched only once.
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.
The payload is a JavaScript object accessible through useNuxtApp().payload
. It is used on the client to avoid refetching the same data when the code is executed in the browser during hydration.
Suspense
Nuxt uses Vue’s <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.
<NuxtLoadingIndicator>
to add a progress bar between page navigations.useFetch
The useFetch
composable is the most straightforward way to perform data fetching.
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Page visits: {{ count }}</p>
</template>
This composable is a wrapper around the useAsyncData
composable and $fetch
utility.
$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.
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
$fetch
will not provide network calls de-duplication and navigation prevention. It is recommended to use
$fetch
for client-side interactions (event based) or combined with useAsyncData
when fetching the initial component data.useAsyncData
The useAsyncData
composable is responsible for wrapping async logic and returning the result once it is resolved.
useFetch(url)
is nearly equivalent to useAsyncData(url, () => $fetch(url))
. It's developer experience sugar for the most common use case.
useFetch
and useAsyncData
.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.
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
// This is also possible:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData
is a unique key used to cache the response of the second argument, the querying function. This key can be ignored by directly passing the querying function, the key will be auto-generated.
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, like when you are creating your own custom composable wrapping useAsyncData
.
Setting a key can be useful to share the same data between components using
useNuxtData
or to refresh specific data.<script setup lang="ts">
const { id } = useRoute().params
const { data, error } = await useAsyncData(`user:${id}`, () => {
return myGetFunction('users', { id })
})
</script>
The useAsyncData
composable is a great way to wrap and wait for multiple $fetch
requests to be completed, and then process the results.
<script setup lang="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 }
})
// discounts.value.coupons
// discounts.value.offers
</script>
Return Values
useFetch
and useAsyncData
have the same return values listed below.
data
: the result of the asynchronous function that is passed in.pending
: a boolean indicating whether the data is still being fetched.refresh
/execute
: a function that can be used to refresh the data returned by thehandler
function.clear
: a function that can be used to setdata
to undefined, seterror
tonull
, setpending
tofalse
, setstatus
toidle
, and mark any currently pending requests as cancelled.error
: an error object if the data fetching failed.status
: a string indicating the status of the data request ("idle"
,"pending"
,"success"
,"error"
).
data
, pending
, error
and status
are Vue refs accessible with .value
in <script setup>
By default, Nuxt waits until a refresh
is finished before it can be executed again.
server: false
), then the data will not be fetched until hydration completes. This means even if you await useFetch
on client-side, data
will remain null within <script setup>
.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 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.
<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
and useLazyAsyncData
as convenient methods to perform the same.
<script setup lang="ts">
const { pending, data: posts } = useLazyFetch('/api/posts')
</script>
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).
/* This call is performed before hydration */
const articles = await useFetch('/api/article')
/* This call will only be performed on the client */
const { pending, data: comments } = 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.
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.
<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.
const { data: mountains } = await useFetch('/api/mountains', {
transform: (mountains) => {
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
}
})
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
and useAsyncData
use keys to prevent refetching the same data.
useFetch
uses the provided URL as a key. Alternatively, akey
value can be provided in theoptions
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 ofuseAsyncData
will be generated for you.
useNuxtData
Refresh and execute
If you want to fetch or refresh data manually, use the execute
or refresh
function provided by the composables.
<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.
clearNuxtData
and refreshNuxtData
.Clear
If you want to clear the data provided, for whatever reason, without needing to know the specific key to pass to clearNuxtData
, you can use the clear
function provided by the composables.
<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')
const route = useRoute()
watch(() => route.path, (path) => {
if (path === '/') clear()
})
</script>
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.
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch('/api/users', {
/* Changing the id will trigger a refetch */
watch: [id]
})
</script>
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.
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id]
})
</script>
If you need to change the URL based on a reactive value, you may want to use a 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.
<script setup lang="ts">
const id = ref(null)
const { data, pending } = useLazyFetch('/api/user', {
query: {
user_id: id
}
})
</script>
In the case of more complex URL construction, you may use a callback as a computed getter 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, and you can wait until the reactive element changes before fetching.
<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.
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.
<script setup lang="ts">
const { data, error, execute, pending, status } = await useLazyFetch('/api/comments', {
immediate: false
})
</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 startedpending
when a fetch has started but not yet completederror
when the fetch failssuccess
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
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.
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
const { data } = await useFetch('/api/me', { headers })
</script>
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.
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
}
<script setup lang="ts">
// This composable will automatically pass cookies to the client
const event = useRequestEvent()
const { data: result } = await useAsyncData(() => 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.
<script>
export default defineNuxtComponent({
/* Use the fetchKey option to provide a unique key */
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello')
}
}
})
</script>
<script setup>
or <script setup lang="ts">
are the recommended way of declaring Vue components in Nuxt 3.Serializing Data From Server to Client
When using useAsyncData
and useLazyAsyncData
to transfer data fetched on server to the client (as well as anything else that utilizes the Nuxt payload), the payload is serialized with devalue
. This allows us to transfer not just basic JSON but also to serialize and revive/deserialize more advanced kinds of data, such as regular expressions, Dates, Map and Set, ref
, reactive
, shallowRef
, shallowReactive
and NuxtError
- and more.
It is also possible to define your own serializer/deserializer for types that are not supported by Nuxt. You can read more in the useNuxtApp
docs.
$fetch
or useFetch
- see the next section for more information.Serializing Data From API Routes
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.
Example
export default defineEventHandler(() => {
return new Date()
})
<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.
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
})
<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 as our serializer.
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
})
<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>
Recipes
Consuming SSE (Server Sent Events) via POST request
EventSource
or VueUse composable useEventSource
.When consuming SSE via POST request, you need to handle the connection manually. Here's how you can do it:
// Make a POST request to the SSE endpoint
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
method: 'POST',
body: {
query: "Hello AI, how are you?",
},
responseType: 'stream',
})
// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
// Read the chunk of data as we get it
while (true) {
const { value, done } = await reader.read()
if (done)
break
console.log('Received:', value)
}