Яка різниця між useFetch та useAsyncData в Nuxt?
useFetch - це composable для HTTP-запитів з автоматичним кешуванням за URL, а useAsyncData виконує будь-який async-код і кешує результат під ключем, який ти задаєш вручну.
Теорія
TL;DR
useFetch- як замовлення в ресторанному застосунку: даєш URL, він сам трекає запит і кешує результат.useAsyncData- як готуєш сам: повний контроль над кожним кроком, ім'я рецепту задаєш ти.- Головна різниця:
useFetchавтоматично генерує ключ кешу з URL і параметрів;useAsyncDataпотребує рядкового ключа від тебе. - Один endpoint?
useFetch. Фільтрація, join або кілька запитів?useAsyncData. - Швидке правило: якщо можна записати як один URL - бери
useFetch.
Швидкий приклад
<!-- useFetch: ключ кешу = '/api/users' -->
<script setup>
const { data: users, pending } = await useFetch('/api/users')
</script>
<!-- useAsyncData: ручний ключ 'active-users', кастомна логіка -->
<script setup>
const { data } = await useAsyncData('active-users', () =>
$fetch('/api/users').then(users => users.filter(u => u.active))
)
</script>useFetch кешує сиру відповідь під URL. useAsyncData кешує відфільтрований результат під 'active-users'. Джерело те саме, але виходи різні, і ключі теж.
Головна відмінність
useFetch перетворює URL і параметри запиту на унікальний ключ кешу автоматично, тому реагує на зміни маршруту без додаткового налаштування. useAsyncData використовує будь-який рядок, який ти передаєш як ключ. Це дає простір для довільного async-коду, але відповідальність за унікальність ключа лежить на тобі. Якщо дві сторінки використовують один ключ із різними формами даних (data shapes), перемагає останній запит і ти отримуєш застарілий кеш.
Коли використовувати
- Прямий запит до одного endpoint:
useFetch('/api/posts') - Динамічні дані маршруту, що змінюються з URL:
useFetch(реагує автоматично) - Фільтрація або трансформація відповіді:
useAsyncData(обгорни$fetchу свою логіку) - Кілька паралельних запитів:
useAsyncDataзPromise.allвсередині обробника - Лише client-side дані на кшталт стану WebSocket:
useAsyncDataзserver: false
Таблиця порівняння
| Характеристика | useFetch | useAsyncData |
|---|---|---|
| Ключ кешу | Авто (хеш URL + параметрів) | Ручний (обов'язковий рядок) |
| Обробник | URL-рядок | Будь-яка async-функція |
| Реактивність | Авто на зміну маршруту | Вручну через ключ |
| Fetcher за замовчуванням | $fetch | Немає, передаєш свій |
| Lazy-режим | Вбудована опція | Вбудована опція |
| SSR payload | Додається автоматично | Додається якщо ключ унікальний |
| Для чого | Прості API-запити | Фільтрація, join, складна логіка |
Як працює кешування
Обидва composable реєструють async-задачі під час SSR, призупиняють рендер до завершення обробника і серіалізують результат в HTML-пейлоад для гідратації (hydration). useFetch обчислює hash(url + options) і зберігає результат в useNuxtApp().payload.data під цим хешем. useAsyncData записує твій рядок у той самий сховок напряму. На клієнті, якщо ключ вже є в пейлоаді, повторного мережевого запиту не відбувається.
Типові помилки
Відсутній ключ у useAsyncData
// Неправильно - ключ обов'язковий в Nuxt 3
const { data } = await useAsyncData(async () => $fetch('/api/posts'))
// Правильно
const { data } = await useAsyncData('posts', async () => $fetch('/api/posts'))Статичний ключ для динамічних даних
// Неправильно - id змінюється, але ключ 'posts' статичний
// При переходах між записами завжди повертаються перші дані
const { data } = await useAsyncData('posts', () => $fetch(`/api/posts/${id}`))
// Правильно - ключ змінюється разом з id
const { data } = await useAsyncData(`posts-${id}`, () => $fetch(`/api/posts/${id}`))Це помилка, яку я найчастіше зустрічав на code review. Людина бачить, що дані не оновлюються при переходах між записами, додає watch, і все одно не розуміє чому не працює. А причина в ключі.
useFetch там, де потрібна трансформація відповіді
// Працює, але data - це сира відповідь без фільтрації
const { data } = await useFetch('/api/search', { body: { q: 'nuxt' } })
// Краще коли потрібен парсинг або фільтрація
const { data } = await useAsyncData('search-nuxt', () =>
$fetch('/api/search', { method: 'POST', body: { q: 'nuxt' } })
.then(res => res.results.filter(r => r.published))
)Де зустрічається в реальних проектах
- Nuxt Content:
useAsyncData('content', () => queryContent().find()) - Nuxt Auth (sidebase):
useFetch('/api/auth/me')для даних сесії - Supabase + Nuxt:
useAsyncData(posts-${userId}, () => supabase.from('posts').select())для row-level security - Інвалідація кешу після мутації:
refreshNuxtData('key')для повторного завантаження,clearNuxtData('key')щоб видалити значення
Питання на співбесіді
Q: Що відбудеться, якщо два компоненти використовують useAsyncData з однаковим ключем, але різними обробниками?
A: Другий виклик поверне закешований результат першого. Для спільних даних це може бути навмисним, але якщо обробники різні - ти отримаєш неправильний результат. Завжди використовуй унікальні ключі для кожної форми даних.
Q: Чи підтримує useFetch POST-запити?
A: Так. Передай method: 'POST' і body як параметри. Але якщо ще й потрібно трансформувати відповідь, краще переключитися на useAsyncData - так зрозуміліше про намір коду.
Q: Що робить опція server: false в useAsyncData?
A: Пропускає обробник під час SSR і виконує його тільки на клієнті. Використовуй для даних, що залежать від браузерних API або не повинні потрапляти в HTML-пейлоад, наприклад live-дані дашборду.
Q: Як інвалідувати кеш useAsyncData після мутації у великому застосунку?
A: Виклич refreshNuxtData('key') для повторного запиту або clearNuxtData('key') щоб повністю видалити значення з кешу. Поєднуй із refresh(), який повертає useFetch, для оптимістичних оновлень.
Q: Як useFetch реагує на зміну реактивних query-параметрів?
A: Передай реактивний об'єкт в опцію query. useFetch відстежує зміни і автоматично повторює запит - це одна з причин обирати його замість $fetch всередині watchEffect.
Приклади
Список продуктів за динамічною категорією
<!-- pages/products/[category].vue -->
<script setup>
const route = useRoute()
// Ключ кешу: '/api/products/electronics?limit=20&sort=price'
// Автоматично оновлюється при зміні route.params.category
const { data: products, pending } = await useFetch(
`/api/products/${route.params.category}`,
{ query: { limit: 20, sort: 'price' } }
)
</script>
<template>
<div v-if="pending">Завантаження...</div>
<ul v-else>
<li v-for="p in products" :key="p.id">{{ p.name }}</li>
</ul>
</template>useFetch тут достатньо, бо URL вже містить всю варіацію. Ручний ключ і watcher не потрібні.
Дашборд з паралельними запитами та обробкою часткових помилок
<script setup>
const { data: userId } = useAuth() // твій auth composable
// Унікальний ключ для кожного користувача, паралельні запити, часткові помилки не ламають все
const { data: dashboard, error } = await useAsyncData(
`dashboard-${userId.value}`,
async () => {
const [stats, recent, notifications] = await Promise.allSettled([
$fetch('/api/stats'),
$fetch('/api/recent-activity'),
$fetch('/api/notifications')
])
// Статистика обов'язкова - кидаємо помилку якщо недоступна
if (stats.status === 'rejected') throw new Error('Stats unavailable')
return {
stats: stats.value,
recent: recent.status === 'fulfilled' ? recent.value : [],
notifications: notifications.status === 'fulfilled' ? notifications.value : []
}
},
{ default: () => null }
)
</script>useAsyncData підходить тут, бо три окремі запити, крок злиття і кешування на рівні кожного користувача - це не те, для чого розроблявся useFetch. Ключ включає userId, тож кожен користувач отримує свій окремий пейлоад.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.