Skip to main content

Яка різниця між useFetch та useAsyncData в Nuxt?

useFetch - це composable для HTTP-запитів з автоматичним кешуванням за URL, а useAsyncData виконує будь-який async-код і кешує результат під ключем, який ти задаєш вручну.

Теорія

TL;DR

  • useFetch - як замовлення в ресторанному застосунку: даєш URL, він сам трекає запит і кешує результат.
  • useAsyncData - як готуєш сам: повний контроль над кожним кроком, ім'я рецепту задаєш ти.
  • Головна різниця: useFetch автоматично генерує ключ кешу з URL і параметрів; useAsyncData потребує рядкового ключа від тебе.
  • Один endpoint? useFetch. Фільтрація, join або кілька запитів? useAsyncData.
  • Швидке правило: якщо можна записати як один URL - бери useFetch.

Швидкий приклад

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

Таблиця порівняння

ХарактеристикаuseFetchuseAsyncData
Ключ кешуАвто (хеш URL + параметрів)Ручний (обов'язковий рядок)
ОбробникURL-рядокБудь-яка async-функція
РеактивністьАвто на зміну маршрутуВручну через ключ
Fetcher за замовчуванням$fetchНемає, передаєш свій
Lazy-режимВбудована опціяВбудована опція
SSR payloadДодається автоматичноДодається якщо ключ унікальний
Для чогоПрості API-запитиФільтрація, join, складна логіка

Як працює кешування

Обидва composable реєструють async-задачі під час SSR, призупиняють рендер до завершення обробника і серіалізують результат в HTML-пейлоад для гідратації (hydration). useFetch обчислює hash(url + options) і зберігає результат в useNuxtApp().payload.data під цим хешем. useAsyncData записує твій рядок у той самий сховок напряму. На клієнті, якщо ключ вже є в пейлоаді, повторного мережевого запиту не відбувається.

Типові помилки

Відсутній ключ у useAsyncData

js
// Неправильно - ключ обов'язковий в Nuxt 3 const { data } = await useAsyncData(async () => $fetch('/api/posts')) // Правильно const { data } = await useAsyncData('posts', async () => $fetch('/api/posts'))

Статичний ключ для динамічних даних

js
// Неправильно - 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 там, де потрібна трансформація відповіді

js
// Працює, але 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.

Приклади

Список продуктів за динамічною категорією

vue
<!-- 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 не потрібні.

Дашборд з паралельними запитами та обробкою часткових помилок

vue
<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, тож кожен користувач отримує свій окремий пейлоад.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?