$fetch: запросы по событиям

$fetch — это запрос «по требованию»: для отправки формы, клика, мутации. В setup при SSR его звать нельзя — будет двойной запрос.
Суть: $fetch — низкоуровневый HTTP-клиент, работающий и на сервере, и на клиенте. Его используют для событийных запросов: отправки формы, POST-мутаций, действий по клику. В setup-функции компонента его напрямую не вызывают — это вызовет двойной запрос при SSR.

Мы разобрали загрузку данных при рендере. Но есть и другой сценарий: пользователь нажал «Купить», отправил форму, лайкнул пост. Это событийные запросы — они происходят не при загрузке страницы, а в ответ на действие. Для них предназначен $fetch.

$fetch — это улучшенный fetch (на базе ofetch): он сам разбирает JSON, бросает ошибку на плохой статус, работает в обеих средах. Его место — в обработчиках событий:

<script setup>
async function buy(productId) {
  await $fetch("/api/cart", {
    method: "POST",
    body: { productId },
  })
  // обновить UI после успешной мутации
}
</script>

<template>
  <button @click="buy(42)">Купить</button>
</template>

Главное правило: не вызывайте $fetch прямо в setup для начальной загрузки. В setup код выполняется и на сервере, и на клиенте, поэтому голый $fetch там запросит данные дважды и сломает экономию payload. Для загрузки при рендере — useFetch/useAsyncData; для событий — $fetch.

   Три инструмента — три задачи

   useFetch      -> начальная загрузка, один URL (SSR-safe)
   useAsyncData  -> начальная загрузка, сложная логика (SSR-safe)
   $fetch        -> событие: POST/клик/форма (императивно)

Отдельно стоит сказать про опцию server: false и ленивые варианты. По умолчанию загрузка происходит на сервере и блокирует навигацию до готовности данных — это даёт полный HTML, но задерживает переход. Иногда это нежелательно: например, для второстепенного блока, без которого страница уже осмысленна. Тогда комбинация lazy и реактивных параметров позволяет показать каркас сразу и дозагрузить данные следом. Понимание этих рычагов превращает useAsyncData из чёрного ящика в точный инструмент управления тем, что и когда загружается.

Как работает под капотом

$fetch не оборачивает запрос в SSR-жизненный цикл и не пишет в payload — он просто выполняет HTTP-вызов здесь и сейчас. Поэтому он не блокирует навигацию и идеально подходит для мутаций. Любопытная деталь: на сервере $fetch к собственному /api/* не делает реального сетевого вызова, а напрямую вызывает обработчик Nitro — быстрее и без накладных расходов сети. Внутри useFetch для самого запроса тоже используется $fetch.

Смоделируем разницу: загрузка через кеширующий composable против событийной мутации через прямой вызов:

// useFetch кеширует, $fetch бьёт по серверу каждый раз.
let serverCalls = 0;
function server(action) { serverCalls++; return "ok:" + action; }

const cache = {};
function useFetch(url) {                       // загрузка: кеш
  if (cache[url]) return cache[url];
  return (cache[url] = server("GET " + url));
}
function $fetch(url, opts) {                    // событие: всегда запрос
  return server((opts && opts.method || "GET") + " " + url);
}

useFetch("/api/products");                      // 1-й запрос
useFetch("/api/products");                      // из кеша, без запроса
$fetch("/api/cart", { method: "POST" });        // мутация -> запрос
$fetch("/api/cart", { method: "POST" });        // ещё клик -> ещё запрос
console.log("Реальных обращений к серверу:", serverCalls);  // 3

Попробуй сам ▶ — загрузка через useFetch не повторяет запрос, а каждый клик с $fetch — это новое обращение. Это ровно то поведение, которое нужно для мутаций.

Частые ошибки

  • $fetch в setup для начальной загрузки. Двойной запрос при SSR. Используйте useFetch.
  • Не ловить ошибку мутации. $fetch бросает исключение на плохой статус — оборачивайте в try/catch.
  • Слать секреты с клиента. Токены и ключи — на сервере, в server/api, а не в браузерном $fetch.

Best practices

  • Событие (клик, submit, мутация) — $fetch; рендер-загрузка — useFetch/useAsyncData.
  • Оборачивайте мутации в try/catch и показывайте пользователю результат.
  • После успешной мутации обновляйте локальное состояние или перезапрашивайте данные через refresh().

Итог: $fetch — императивный инструмент для запросов по событиям, дополняющий декларативные useFetch/useAsyncData. Зная, когда что брать, вы избегаете двойных запросов. Дальше — обработка состояний загрузки и ошибок.

Проверьте себя
1. Для какого сценария предназначен $fetch?
AНачальная загрузка данных при рендере страницы
BСобытийные запросы: отправка формы, POST-мутации, действия по клику
CТолько для генерации статических страниц
DДля настройки маршрутов
2. Почему нельзя вызывать $fetch напрямую в setup для начальной загрузки при SSR?
A$fetch не поддерживает JSON
BSetup выполняется и на сервере, и на клиенте, поэтому голый $fetch запросит данные дважды и сломает экономию payload
C$fetch работает только в Nuxt 2
DЭто вызовет утечку памяти на клиенте