useFetch: данные без двойных запросов

useFetch грузит данные на сервере, кладёт их в payload и не запрашивает повторно в браузере — главный инструмент получения данных в Nuxt.
Суть: useFetch — композабл для загрузки данных по URL. При SSR он выполняет запрос на сервере, передаёт результат в payload, и клиент не делает повторный запрос. Возвращает реактивные data, pending (status) и error.

Получение данных — то, ради чего часто и берут фулстек-фреймворк. Казалось бы, можно просто вызвать fetch() в компоненте. Но при SSR это создаёт проблему: запрос выполнится дважды — сначала на сервере (чтобы отрендерить HTML), потом в браузере (при гидратации). Двойная нагрузка, лишний трафик, мигание данных.

useFetch решает это. Он выполняет запрос один раз на сервере, кладёт ответ в payload, а на клиенте берёт готовые данные оттуда — без повторного обращения к API. Использование предельно лаконично:

<script setup>
const { data, pending, error } = await useFetch("/api/products")
</script>

<template>
  <p v-if="pending">Загрузка...</p>
  <p v-else-if="error">Ошибка: {{ error.message }}</p>
  <ul v-else>
    <li v-for="p in data" :key="p.id">{{ p.name }}</li>
  </ul>
</template>

useFetch возвращает реактивные ссылки: data — результат, pending (в новых версиях — status) — флаг загрузки, error — ошибка. Это позволяет аккуратно показать индикатор загрузки, сообщение об ошибке или сами данные.

   useFetch при SSR (без двойного запроса)

   Сервер: useFetch("/api/products")
            |-- запрос к API -> данные
            |-- рендер HTML с данными
            |-- данные -> payload
   Браузер: useFetch("/api/products")
            |-- видит данные в payload -> берёт их
            |-- НЕ запрашивает повторно

Когда состояние перерастает простой useState — появляются десятки полей, сложные действия, взаимозависимости — стоит присмотреться к Pinia, официальному хранилищу для Vue и Nuxt. Pinia даёт структурированные сторы с состоянием, геттерами и действиями, отличную поддержку типов и инструменты отладки, при этом оставаясь SSR-безопасной. Граница простая: useState — для лёгкого общего состояния вроде темы оформления или корзины, Pinia — когда логики становится много и хочется навести в ней порядок. Начинать почти всегда стоит с useState и мигрировать по мере роста.

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

useFetch — это удобная обёртка над useAsyncData и низкоуровневым $fetch. Он автоматически генерирует ключ кеша из URL и опций, чтобы связать серверный и клиентский вызовы. По умолчанию useFetch блокирует навигацию до завершения запроса — поэтому к нему ставят await. Результат сохраняется в payload по этому ключу; клиент находит его и переиспользует.

Смоделируем механику «не запрашивать дважды» через простой кеш по ключу:

// Имитация useFetch: запрос только если в payload пусто.
const payload = {};                    // то, что сервер передал клиенту
let realRequests = 0;

function fakeApi(url) {
  realRequests++;                       // считаем реальные обращения
  return "данные для " + url;
}

function useFetch(url, env) {
  if (env === "client" && payload[url] !== undefined) {
    return { data: payload[url], from: "payload" };   // взяли готовое
  }
  const data = fakeApi(url);
  if (env === "server") payload[url] = data;          // положили в payload
  return { data, from: env };
}

console.log(useFetch("/api/products", "server"));  // реальный запрос
console.log(useFetch("/api/products", "client"));  // из payload
console.log("Реальных запросов к API:", realRequests);  // 1, не 2

Попробуй сам ▶ — несмотря на два «вызова» (сервер и клиент), реальный запрос к API всего один. В этом вся ценность useFetch.

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

  • Использовать голый fetch() в setup. Это даёт двойной запрос и проблемы гидратации. Берите useFetch.
  • Забыть await. Без него данные могут не успеть к серверному рендеру.
  • Дёргать useFetch по клику. Это композабл для загрузки при рендере; для событий используйте $fetch (отдельный урок).

Best practices

  • Для начальной загрузки страницы по одному GET-эндпоинту — useFetch с await.
  • Всегда обрабатывайте pending/status и error в шаблоне — пользователь не должен видеть «зависшую» страницу.
  • Не дублируйте на клиенте то, что уже пришло в payload.

Итог: useFetch — основной способ грузить данные в Nuxt без двойных запросов. Он выполняет запрос на сервере и передаёт результат клиенту через payload. Дальше — его старший брат для нестандартных случаев: useAsyncData.

Проверьте себя
1. Какую проблему решает useFetch по сравнению с обычным fetch() в setup при SSR?
AДелает запросы быстрее в два раза
BИзбегает двойного запроса: данные грузятся на сервере, кладутся в payload и не запрашиваются повторно на клиенте
CШифрует ответ сервера
DРаботает только в браузере
2. Что возвращает useFetch для управления состоянием загрузки?
AТолько готовые данные
BРеактивные data, pending/status и error
CПромис без состояний
DСтроку HTML