Получение данных
В Nuxt есть два композабла и встроенная библиотека для запросов в браузере и на сервере: useFetch, useAsyncData и $fetch.
Кратко:
$fetch— самый простой способ выполнить сетевой запрос.useFetch— обёртка над$fetch, запрос выполняется один раз при универсальном рендеринге.useAsyncData— похож наuseFetch, но даёт более тонкий контроль.
У useFetch и useAsyncData общий набор опций и паттернов — они описаны в конце раздела.
Зачем нужны useFetch и useAsyncData
Nuxt может выполнять изоморфный (универсальный) код и на сервере, и на клиенте. Если в setup-функции компонента Vue использовать только $fetch, данные могут запрашиваться дважды: на сервере (при рендере HTML) и на клиенте (при гидрации). Это приводит к ошибкам гидрации, замедлению и нестабильному поведению.
Композаблы useFetch и useAsyncData решают это: данные, полученные на сервере, передаются на клиент в payload.
Payload — JavaScript-объект, доступный через useNuxtApp().payload. На клиенте он используется, чтобы не запрашивать те же данные повторно при гидрации.
<script setup lang="ts">
const { data } = await useFetch('/api/data')
async function handleFormSubmit () {
const res = await $fetch('/api/submit', {
method: 'POST',
body: {
// My form data
},
})
}
</script>
<template>
<div v-if="data == undefined">
No data
</div>
<div v-else>
<form @submit="handleFormSubmit">
<!-- form input tags -->
</form>
</div>
</template>
В примере выше useFetch гарантирует, что запрос выполнится на сервере и результат попадёт в браузер. У $fetch такого механизма нет — его лучше использовать для запросов только с клиента.
Suspense
Nuxt под капотом использует компонент Vue <Suspense>, чтобы не переходить на страницу до готовности асинхронных данных. Композаблы получения данных работают с этой возможностью и позволяют выбирать поведение для каждого вызова.
<NuxtLoadingIndicator>.$fetch
В Nuxt встроена библиотека ofetch, она автоимпортируется как $fetch по всему приложению.
<script setup lang="ts">
async function addTodo () {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
},
})
}
</script>
$fetch не даёт дедупликации запросов и блокировки навигации. Рекомендуется использовать
$fetch для клиентских действий (по событиям) или вместе с useAsyncData при загрузке начальных данных компонента.Передача заголовков клиента в API
При вызове useFetch на сервере Nuxt использует useRequestFetch для проксирования заголовков и cookie клиента (кроме заголовков вроде host, которые не должны пересылаться).
<script setup lang="ts">
const { data } = await useFetch('/api/echo')
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))
Либо можно использовать useRequestHeaders, чтобы получить cookie и отправить их в API при серверном запросе (инициированном с клиента). Изоморфный вызов $fetch с этими заголовками даёт API тот же заголовок cookie, что и браузер пользователя. Это нужно только если вы не используете useFetch.
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser () {
return await $fetch('/api/me', { headers })
}
</script>
useRequestFetch.host,acceptcontent-length,content-md5,content-typex-forwarded-host,x-forwarded-port,x-forwarded-protocf-connecting-ip,cf-ray
useFetch
Композабл useFetch внутри использует $fetch для SSR-безопасных запросов в setup-функции.
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Page visits: {{ count }}</p>
</template>
Это обёртка над композаблом useAsyncData и утилитой $fetch.
useAsyncData
Композабл useAsyncData оборачивает асинхронную логику и возвращает результат после его разрешения.
useFetch(url) по сути эквивалентен useAsyncData(url, () => event.$fetch(url)). Это удобная обёртка для типичного сценария. (Подробнее про
event.fetch — в useRequestFetch.)useFetch не всегда уместен — например, когда CMS или сторонний сервис даёт свой слой запросов. В таких случаях оборачивайте вызовы в useAsyncData, сохраняя преимущества композабла.
<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 — уникальный ключ для кэширования ответа функции запроса (второй аргумент). Можно передать только функцию, тогда ключ сгенерируется автоматически.
Автогенерируемый ключ зависит только от файла и строки вызова. Рекомендуется задавать свой ключ, особенно при обёртке
useAsyncData в собственном композабле.
Ключ нужен для общего доступа к данным через
useNuxtData или обновления конкретных данных.<script setup lang="ts">
const { id } = useRoute().params
const { data, error } = await useAsyncData(`user:${id}`, () => {
return myGetFunction('users', { id })
})
</script>
Композабл useAsyncData удобен, когда нужно выполнить несколько запросов $fetch и обработать результат.
<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async (_nuxtApp, { signal }) => {
const [coupons, offers] = await Promise.all([
$fetch('/cart/coupons', { signal }),
$fetch('/cart/offers', { signal }),
])
return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
useAsyncData предназначен для загрузки и кэширования данных, а не для побочных эффектов (например, вызова экшенов Pinia) — это может привести к повторным вызовам с null. Для побочных эффектов используйте утилиту callOnce.<script setup lang="ts">
const offersStore = useOffersStore()
// так делать не стоит
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
Возвращаемые значения
useFetch и useAsyncData возвращают один и тот же набор полей:
data: результат переданной асинхронной функции.refresh/execute: функция для повторной загрузки данных изhandler.clear: функция сбрасываетdataвundefined(или вoptions.default(), если задано), обнуляетerror, выставляетstatusвidleи отменяет текущие запросы.error: объект ошибки при неудачной загрузке.status: строка состояния запроса ("idle","pending","success","error").
data, error и status — Vue ref, в <script setup> доступны через .value.По умолчанию Nuxt ждёт завершения refresh, прежде чем выполнить его снова.
server: false), они не будут загружены до завершения гидрации. Даже при await useFetch на клиенте data останется null внутри <script setup>.Опции
useAsyncData и useFetch возвращают один и тот же тип объекта и принимают общий набор опций последним аргументом. С их помощью можно управлять поведением: блокировкой навигации, кэшированием и моментом выполнения.
Lazy (ленивая загрузка)
По умолчанию композаблы загрузки данных ждут разрешения асинхронной функции перед переходом на новую страницу (благодаря Vue Suspense). На клиенте это можно отключить опцией lazy. Тогда состояние загрузки нужно обрабатывать вручную через status.
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
lazy: true,
})
</script>
<template>
<!-- нужно обработать состояние загрузки -->
<div v-if="status === 'pending'">
Загрузка ...
</div>
<div v-else>
<div v-for="post in posts">
<!-- do something -->
</div>
</div>
</template>
Можно использовать useLazyFetch и useLazyAsyncData для того же поведения.
<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
Загрузка только на клиенте
По умолчанию композаблы выполняют асинхронную функцию и на клиенте, и на сервере. Опция server: false ограничивает выполнение только клиентом. При первой загрузке данные не будут запрошены до завершения гидрации — нужно обрабатывать состояние ожидания; при последующих переходах на клиенте данные будут загружаться до отображения страницы.
Вместе с опцией lazy это удобно для данных, не нужных при первом рендере (например, не влияющих на SEO).
/* Вызов выполняется до гидрации */
const articles = await useFetch('/api/article')
/* Этот запрос выполнится только на клиенте */
const { status, data: comments } = useFetch('/api/comments', {
lazy: true,
server: false,
})
Композабл useFetch нужно вызывать в setup или на верхнем уровне в хуках жизненного цикла; иначе используйте метод $fetch.
Уменьшение размера полезной нагрузки
Опция pick уменьшает объём данных в HTML, возвращая только нужные поля.
<script setup lang="ts">
/* только поля, используемые в шаблоне */
const { data: mountain } = await useFetch('/api/mountains/everest', {
pick: ['title', 'description'],
})
</script>
<template>
<h1>{{ mountain.title }}</h1>
<p>{{ mountain.description }}</p>
</template>
Для более гибкой обработки или преобразования нескольких объектов используйте функцию transform.
const { data: mountains } = await useFetch('/api/mountains', {
transform: (mountains) => {
return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
},
})
pick и transform не отменяют первоначальную загрузку данных, но не добавляют лишние данные в payload при передаче с сервера на клиент.Кэширование и повторная загрузка
Ключи
useFetch и useAsyncData используют ключи, чтобы не запрашивать одни и те же данные повторно.
useFetchиспользует URL как ключ. Либо можно передатьkeyв объекте опций последним аргументом.useAsyncDataиспользует первый аргумент как ключ, если это строка. Если передан handler, ключ генерируется по имени файла и номеру строки.
useNuxtData.Общее состояние и согласованность опций
При одном и том же ключе у нескольких компонентов общими будут refs data, error и status. Чтобы всё работало предсказуемо, часть опций должна совпадать.
Должны совпадать при одном ключе:
- функция
handler - опция
deep - функция
transform - массив
pick - функция
getCachedData - значение
default
// ❌ Вызовет предупреждение в режиме разработки
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: false })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: true })
Могут различаться без предупреждений:
serverlazyimmediatededupewatch
// ✅ Допустимо
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: true })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: false })
Для независимых экземпляров используйте разные ключи:
// Полностью независимые экземпляры
const { data: users1 } = useAsyncData('users-1', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
const { data: users2 } = useAsyncData('users-2', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
Реактивные ключи
В качестве ключа можно передать computed ref, обычный ref или getter — тогда загрузка будет обновляться при изменении зависимостей:
// Ключ как computed
const userId = ref('123')
const { data: user } = useAsyncData(
computed(() => `user-${userId.value}`),
() => fetchUser(userId.value),
)
// При смене userId данные перезапросятся,
// старые удалятся, если больше нигде не используются
userId.value = '456'
Refresh и execute
Чтобы запросить или обновить данные вручную, используйте execute или refresh, возвращаемые композаблами.
<script setup lang="ts">
const { data, error, execute, refresh } = await useFetch('/api/users')
</script>
<template>
<div>
<p>{{ data }}</p>
<button @click="() => refresh()">
Обновить данные
</button>
</div>
</template>
execute — алиас для refresh, удобен, когда загрузка не выполняется сразу.
clearNuxtData и refreshNuxtData.Clear (очистка)
Чтобы очистить данные без вызова clearNuxtData с конкретным ключом, используйте функцию clear от композабла.
<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')
const route = useRoute()
watch(() => route.path, (path) => {
if (path === '/') {
clear()
}
})
</script>
Watch (наблюдение)
Чтобы перезапускать загрузку при изменении реактивных значений, используйте опцию watch с одним или несколькими источниками.
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch('/api/users', {
/* Изменение id вызовет повторный запрос */
watch: [id],
})
</script>
Наблюдение за значением не меняет URL запроса. В примере ниже будет всегда запрашиваться один и тот же начальный id, так как URL формируется в момент вызова:
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id],
})
</script>
Чтобы URL зависел от реактивного значения, используйте computed URL.
При реактивных опциях запроса они по умолчанию отслеживаются и вызывают повторную загрузку. Отключить это можно через watch: false.
const id = ref(1)
// Не будет автоматически перезапрашивать при смене id
const { data, execute } = await useFetch('/api/users', {
query: { id }, // id по умолчанию отслеживается
watch: false,
})
// повторный запрос не выполнится
id.value = 2
Вычисляемый URL
Если URL нужно собирать из реактивных значений и обновлять данные при их смене, можно передать параметры как реактивные значения — Nuxt будет подставлять их и перезапрашивать при изменениях.
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch('/api/user', {
query: {
user_id: id,
},
})
</script>
Для сложного URL можно передать computed getter, возвращающий строку URL.
При изменении зависимости данные запрашиваются по новому URL. В сочетании с not-immediate загрузка начнётся только после изменения значения.
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
immediate: false,
})
const pending = computed(() => status.value === 'pending')
</script>
<template>
<div>
<!-- поле отключено во время загрузки -->
<input
v-model="id"
type="number"
:disabled="pending"
>
<div v-if="status === 'idle'">
Введите ID пользователя
</div>
<div v-else-if="pending">
Загрузка ...
</div>
<div v-else>
{{ data }}
</div>
</div>
</template>
Принудительное обновление при смене других значений: watch.
Не сразу (not immediate)
По умолчанию useFetch начинает загрузку при вызове. Чтобы отложить до действия пользователя, задайте immediate: false.
В этом случае понадобятся status для отображения состояния и execute для запуска загрузки.
<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('/api/comments', {
immediate: false,
})
</script>
<template>
<div v-if="status === 'idle'">
<button @click="execute">
Загрузить данные
</button>
</div>
<div v-else-if="status === 'pending'">
Загрузка комментариев...
</div>
<div v-else>
{{ data }}
</div>
</template>
Значения status:
idle— запрос ещё не запущенpending— запрос выполняетсяerror— ошибкаsuccess— загрузка завершена успешно
Передача заголовков и cookies
При вызове $fetch в браузере заголовки пользователя (например cookie) отправляются в API.
При SSR из соображений безопасности $fetch по умолчанию не передаёт cookies браузера и не пробрасывает cookies из ответа.
При вызове useFetch с относительным URL на сервере Nuxt использует useRequestFetch для проксирования заголовков и cookies (кроме заголовков вроде host).
Проброс cookies из серверного API в ответе SSR
Чтобы пробросить cookies из внутреннего запроса обратно клиенту, это нужно реализовать вручную.
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* Ответ от серверного эндпоинта */
const res = await $fetch.raw(url)
/* Cookies из ответа */
const cookies = res.headers.getSetCookie()
/* Добавляем каждый cookie к входящему ответу */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
}
return res._data
}
<script setup lang="ts">
// Этот композабл автоматически передаёт cookies клиенту
const event = useRequestEvent()
const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))
onMounted(() => console.log(document.cookie))
</script>
Поддержка Options API
В Options API можно использовать загрузку в стиле asyncData, обернув компонент в defineNuxtComponent.
<script>
export default defineNuxtComponent({
/* Уникальный ключ через опцию fetchKey */
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello'),
}
},
})
</script>
<script setup> или <script setup lang="ts">.Сериализация данных с сервера на клиент
При передаче данных с сервера на клиент через useAsyncData и useLazyAsyncData (и при использовании payload Nuxt) payload сериализуется с помощью devalue. Это позволяет передавать не только JSON, но и регулярные выражения, Date, Map, Set, ref, reactive, shallowRef, shallowReactive, NuxtError и др.
Для неподдерживаемых типов можно задать свой сериализатор/десериализатор. Подробнее в документации useNuxtApp.
server/, запрошенным через $fetch или useFetch — см. следующий раздел.Сериализация данных из API-маршрутов
При запросе данных из директории server ответ сериализуется через JSON.stringify. Так как поддерживаются только примитивные типы JavaScript, Nuxt приводит тип возвращаемого значения $fetch и useFetch к фактическому значению.
Пример
export default defineEventHandler(() => {
return new Date()
})
<script setup lang="ts">
// Тип `data` выводится как string, хотя мы вернули Date
const { data } = await useFetch('/api/foo')
</script>
Собственная функция сериализации
Чтобы изменить сериализацию, определите у возвращаемого объекта метод toJSON. Nuxt будет использовать тип возвращаемого значения и не станет приводить типы.
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">
// Тип `data` выводится как { createdAt: { year, month, day } }
const { data } = await useFetch('/api/bar')
</script>
Альтернативный сериализатор
Nuxt не поддерживает замену JSON.stringify. Можно вернуть payload строкой и использовать метод toJSON для сохранения типов.
В примере ниже используется superjson.
import superjson from 'superjson'
export default defineEventHandler(() => {
const data = {
createdAt: new Date(),
// Обход приведения типов
toJSON () {
return this
},
}
// Сериализация в строку через superjson
return superjson.stringify(data) as unknown as typeof data
})
<script setup lang="ts">
import superjson from 'superjson'
// `data` выводится как { createdAt: Date }, можно использовать методы Date
const { data } = await useFetch('/api/superjson', {
transform: (value) => {
return superjson.parse(value as unknown as string)
},
})
</script>
Рецепты
SSE (Server-Sent Events) через POST
EventSource или композабл VueUse useEventSource.При POST-запросе к SSE-эндпоинту соединение нужно обрабатывать вручную:
// POST-запрос к SSE-эндпоинту
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
method: 'POST',
body: {
query: 'Hello AI, how are you?',
},
responseType: 'stream',
})
// ReadableStream с TextDecoderStream для чтения текста
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
while (true) {
const { value, done } = await reader.read()
if (done) { break }
console.log('Received:', value)
}
Параллельные запросы
Если запросы не зависят друг от друга, их можно выполнять параллельно через Promise.all():
const { data } = await useAsyncData((_nuxtApp, { signal }) => {
return Promise.all([
$fetch('/api/comments/', { signal }),
$fetch('/api/author/12', { signal }),
])
})
const comments = computed(() => data.value?.[0])
const author = computed(() => data.value?.[1])