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.