$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. Зная, когда что брать, вы избегаете двойных запросов. Дальше — обработка состояний загрузки и ошибок.