Перейти к содержимому

7. useFetch и useAsyncData

Получение данных — сердце любого приложения. Nuxt 3 предоставляет несколько мощных инструментов для этого, каждый со своей нишей. Главное отличие от обычного fetch — они работают и на сервере (SSR), и на клиенте, передавая данные без дополнительного запроса.


// 1. useFetch — для простых случаев
const { data } = await useFetch('/api/users')
// 2. useAsyncData — для сложных случаев с контролем
const { data } = await useAsyncData('users', () => $fetch('/api/users'))
// 3. $fetch — для простых запросов без SSR нужд
const user = await $fetch('/api/users/1')

pages/users/index.vue
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
// Базовое использование
const { data, pending, error, refresh } = await useFetch<User[]>('/api/users')
// С опциями
const { data: user } = await useFetch<User>('/api/users/1', {
// HTTP метод
method: 'GET',
// Query параметры
query: { page: 1, limit: 10 },
// Заголовки
headers: { 'Authorization': 'Bearer token' },
// Тело запроса (для POST/PUT)
body: { name: 'John' },
// Кэширование: уникальный ключ
key: 'user-1',
// Lazy — не блокирует рендеринг страницы
lazy: true,
// Не запускать сразу
immediate: false,
// Трансформация данных
transform: (data) => data.map(user => ({
...user,
fullName: \`\${user.firstName} \${user.lastName}\`
})),
// Дефолтное значение пока загружается
default: () => [],
// Только на сервере
server: true,
// Только на клиенте
// server: false,
// Коллбэки
onRequest({ request, options }) {
console.log('Запрос:', request)
},
onResponse({ response }) {
console.log('Ответ:', response.status)
},
onRequestError({ error }) {
console.error('Ошибка запроса:', error)
},
onResponseError({ error }) {
console.error('Ошибка ответа:', error)
},
})
</script>

<script setup lang="ts">
const page = ref(1)
const search = ref('')
// URL реагирует на изменения page и search!
const { data, pending } = await useFetch('/api/posts', {
query: {
page, // Реактивный!
search, // Реактивный!
limit: 10, // Статичный
}
})
// При изменении page или search — автоматический повторный запрос
</script>
<template>
<input v-model="search" placeholder="Поиск..." />
<div v-if="pending">Загрузка...</div>
<PostList v-else :posts="data" />
<button @click="page++">Следующая страница</button>
</template>

<script setup lang="ts">
// useAsyncData позволяет использовать любую async функцию
const { data: posts } = await useAsyncData(
// Уникальный ключ (важен для дедупликации и кэша!)
'blog-posts',
// Любая асинхронная функция
async () => {
const [posts, categories] = await Promise.all([
$fetch('/api/posts'),
$fetch('/api/categories'),
])
return { posts, categories }
},
// Опции (такие же как у useFetch)
{
lazy: false,
default: () => ({ posts: [], categories: [] }),
transform: (data) => ({
...data,
posts: data.posts.map(p => ({
...p,
date: new Date(p.createdAt).toLocaleDateString('ru'),
}))
})
}
)
</script>

const {
data, // Ref<T> — данные ответа
pending, // Ref<boolean> — идёт загрузка?
error, // Ref<Error | null> — ошибка
status, // Ref<'idle' | 'pending' | 'success' | 'error'>
refresh, // () => Promise<void> — повторить запрос
execute, // () => Promise<void> — выполнить (для immediate: false)
clear, // () => void — сбросить данные
} = await useFetch('/api/data')

<script setup>
const { data: todos, refresh } = await useFetch('/api/todos')
const addTodo = async (text: string) => {
// Оптимистично добавляем в UI
todos.value?.push({ id: Date.now(), text, done: false })
try {
await $fetch('/api/todos', {
method: 'POST',
body: { text }
})
} catch {
// При ошибке — откатываемся
await refresh()
}
}
</script>
<script setup>
const currentPage = ref(1)
const { data, pending } = await useFetch('/api/posts', {
query: { page: currentPage, limit: 10 },
watch: [currentPage], // Следить за изменениями
})
</script>
<script setup>
const userId = ref<number | null>(null)
const { data: user } = await useFetch(
() => userId.value ? \`/api/users/\${userId.value}\` : null
)
// Запрос не выполняется если userId === null
</script>

// Простой запрос (не создаёт SSR-гидрацию)
const user = await $fetch('/api/users/1')
// POST запрос
const created = await $fetch('/api/posts', {
method: 'POST',
body: {
title: 'Новый пост',
content: 'Содержимое...'
}
})
// С обработкой ошибок
try {
const data = await $fetch('/api/protected', {
headers: {
Authorization: \`Bearer \${token}\`
}
})
} catch (error) {
if (error.status === 401) {
navigateTo('/login')
}
}

plugins/api.ts
export default defineNuxtPlugin(() => {
const api = $fetch.create({
baseURL: useRuntimeConfig().public.apiBase,
onRequest({ options }) {
// Добавляем токен к каждому запросу
const token = useCookie('auth-token')
if (token.value) {
options.headers = {
...options.headers,
Authorization: \`Bearer \${token.value}\`
}
}
},
onResponseError({ response }) {
if (response.status === 401) {
navigateTo('/login')
}
}
})
return {
provide: {
api // Теперь доступен как $api
}
}
})

server/api/combined.ts
export default defineEventHandler(async (event) => {
// Используем для запросов на том же сервере
const requestFetch = useRequestFetch()
const [users, posts] = await Promise.all([
requestFetch('/api/users'),
requestFetch('/api/posts'),
])
return { users, posts }
})

// Ключ определяет кэш — одинаковый ключ = один запрос
// Даже если вызвать на разных страницах!
// Page A
const { data } = await useFetch('/api/users', { key: 'users-list' })
// Page B — НЕ делает новый запрос, берёт из кэша!
const { data } = await useFetch('/api/users', { key: 'users-list' })
// Сброс кэша при мутации
const { data, refresh } = await useFetch('/api/todos', { key: 'todos' })
const deleteTodo = async (id: number) => {
await $fetch(\`/api/todos/\${id}\`, { method: 'DELETE' })
// Обновляем данные
await refresh()
}

<script setup>
const { data, error, pending } = await useFetch('/api/data', {
// Не бросать ошибку — обработаем вручную
// (по умолчанию useFetch бросает ошибку при !2xx)
})
</script>
<template>
<div v-if="pending">⏳ Загрузка...</div>
<div v-else-if="error">
❌ Ошибка: {{ error.message }}
<br>
Статус: {{ error.statusCode }}
</div>
<div v-else>
✅ {{ data }}
</div>
</template>