7. useFetch и useAsyncData
📡 Data Fetching: useFetch и useAsyncData
Заголовок раздела «📡 Data Fetching: 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')useFetch — основной инструмент 🚀
Заголовок раздела «useFetch — основной инструмент 🚀»<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>useFetch с реактивными параметрами 🔄
Заголовок раздела «useFetch с реактивными параметрами 🔄»<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>useAsyncData — расширенный контроль 🎛️
Заголовок раздела «useAsyncData — расширенный контроль 🎛️»<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>$fetch — для клиентских запросов ⚡
Заголовок раздела «$fetch — для клиентских запросов ⚡»// Простой запрос (не создаёт 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') }}Интерцепторы $fetch 🔗
Заголовок раздела «Интерцепторы $fetch 🔗»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 } }})useRequestFetch — серверные запросы 🖥️
Заголовок раздела «useRequestFetch — серверные запросы 🖥️»export default defineEventHandler(async (event) => { // Используем для запросов на том же сервере const requestFetch = useRequestFetch()
const [users, posts] = await Promise.all([ requestFetch('/api/users'), requestFetch('/api/posts'), ])
return { users, posts }})Кэширование и дедупликация 💾
Заголовок раздела «Кэширование и дедупликация 💾»// Ключ определяет кэш — одинаковый ключ = один запрос// Даже если вызвать на разных страницах!
// Page Aconst { 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>