Skip to content

Latest commit

 

History

History
539 lines (406 loc) · 16.1 KB

6.data-fetching.md

File metadata and controls

539 lines (406 loc) · 16.1 KB
navigation.icon description
uil:channel
Nuxt provides composables to handle data fetching within your application.

Data Fetching

Nuxt provides useFetch, useLazyFetch, useAsyncData and useLazyAsyncData to handle data fetching within your application.

::alert{icon=👉} useFetch, useLazyFetch, useAsyncData and useLazyAsyncData only work during setup 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

<script setup>
const { data: count } = await useFetch('/api/count')
</script>

<template>
  Page visits: {{ count }}
</template>

::LinkExample{link="/docs/examples/composables/use-fetch"} ::

useLazyFetch

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).

::ReadMore{link="/docs/api/composables/use-lazy-fetch"} ::

Example

<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>

<script setup>
const { pending, data: posts } = useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts starts out null, you will not have access
  // to its contents immediately, but you can watch it.
})
</script>

useAsyncData

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

let counter = 0
export default defineEventHandler(() => {
  counter++
  return counter
})
<script setup>
const { data } = await useAsyncData('count', () => $fetch('/api/count'))
</script>

<template>
  Page visits: {{ data }}
</template>

::LinkExample{link="/docs/examples/composables/use-async-data"} ::

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

<template>
  <div>
    {{ pending ? 'Loading' : count }}
  </div>
</template>

<script setup>
const { pending, data: count } = 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.
})
</script>

Transforming Data

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 section.

Example

<script setup>
const { data: users } = await useAsyncData(
  "users",
  () => $fetch('/api/users'),
  {
    transform: (users) =>
      users.map((user) => ({
        id: user.id,
        fullName: `${user.firstName} ${user.surname}`,
      })),
  }
);
</script>

<template>
  <div>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.fullName }}
      </li>
    </ul>
  </div>
</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.

<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:

<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.

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

<template>
  <div>
    {{ pending ? 'Loading' : count }}
  </div>
  <button @click="refresh">Refresh</button>
</template>

<script setup>
const { pending, data: count } = 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.

<script>
export default defineNuxtComponent({
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello')
    }
  }
})
</script>

::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"} ::

Using $fetch directly

There are instances where you may need to directly call the API. Nuxt 3 provides a globally available $fetch method using unjs/ofetch (in addition to fetch) with the same API as the Fetch API.

Using $fetch has a number of benefits, including:

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

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

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>
const headers = useRequestHeaders(['cookie'])
const { data } = await useFetch('/api/me', { headers })
</script>

::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 ::

Example: 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) => {
  const res = await $fetch.raw(url)
  const cookies = (res.headers.get('set-cookie') || '').split(',')
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  return res._data
}
<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>

Best Practices

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:

{
  "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 China–Nepal 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:

<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).

<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.

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().

<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 ::

<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`)
    ])

    return {
      organization,
      repos
    }
  }
})
</script>

<template>
  <header>
    <h1>{{ organization.login }}</h1>
    <p>{{ organization.description }}</p>
  </header>
</template>

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.

::alert{icon=👉} You can learn more about JSON.stringify limitations here. ::

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>