Загрузка, ошибки и обновление данных

Данные приходят не мгновенно и не всегда: грамотный UI показывает загрузку, обрабатывает ошибку и умеет перезапрашивать.
Суть: useFetch/useAsyncData возвращают не только data, но и status (pending), error и функцию refresh. Опция lazy не блокирует навигацию, watch перезапрашивает при смене параметров, а createError отдаёт корректную страницу ошибки при SSR.

Запрос данных — это всегда три исхода: ещё грузится, успешно пришло, упало с ошибкой. Хороший интерфейс обрабатывает все три. Nuxt даёт для этого готовые реактивные состояния, и грех ими не пользоваться.

Помимо data, композаблы возвращают status (или pending), error и refresh. Базовый паттерн в шаблоне мы уже видели; теперь добавим перезапрос и реактивность параметров:

<script setup>
const page = ref(1)
// watch: при смене page данные перезапросятся автоматически
const { data, status, error, refresh } = await useFetch(
  "/api/products",
  { query: { page }, lazy: true }
)
</script>

<template>
  <button @click="refresh()">Обновить</button>
  <button @click="page++">Следующая страница</button>
</template>

Опция lazy: true говорит «не блокируй навигацию ожиданием» — страница покажется сразу, а данные дольются. Когда реактивный параметр (тут page внутри query) меняется, useFetch сам перезапрашивает — это встроенный watch. А refresh() позволяет перезапросить вручную, например после мутации.

Для ошибок при SSR есть createError: он формирует ошибку с HTTP-статусом, и Nuxt показывает соответствующую страницу ошибки (например, 404 для несуществующего товара) с правильным кодом для поисковика.

<script setup>
const { data } = await useFetch("/api/product/" + id)
if (!data.value) {
  throw createError({ statusCode: 404, statusMessage: "Не найдено" })
}
</script>

Есть изящная деталь, которая многих удивляет: когда вы на сервере вызываете $fetch к собственному эндпоинту /api/..., реального сетевого запроса не происходит. Nitro понимает, что обработчик живёт в том же процессе, и вызывает его напрямую — как обычную функцию. Это экономит и время на установление соединения, и ресурсы. На клиенте же тот же $fetch делает настоящий HTTP-запрос. Один и тот же вызов ведёт себя оптимально в обеих средах, и вам не нужно думать об этом — Nuxt берёт оптимизацию на себя.

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

Статус — это конечный автомат: idle -> pending -> success | error. Композабл переключает его и обновляет реактивные ссылки, заставляя шаблон перерисоваться. refresh сбрасывает статус обратно в pending и заново выполняет загрузку. Реактивные параметры в query/body отслеживаются: их изменение триггерит перезапрос. createError с statusCode при SSR прерывает рендер и отдаёт страницу ошибки с корректным HTTP-кодом.

Смоделируем этот автомат состояний загрузки и перезапрос:

// Конечный автомат состояния запроса.
function createResource(fetcher) {
  const state = { status: "idle", data: null, error: null };
  async function load() {
    state.status = "pending";
    try {
      state.data = await fetcher();
      state.status = "success";
      state.error = null;
    } catch (e) {
      state.status = "error";
      state.error = String(e);
    }
    return { ...state };
  }
  return { load, get: () => ({ ...state }) };
}

(async () => {
  let flaky = 0;
  const res = createResource(async () => {
    flaky++;
    if (flaky === 1) throw new Error("сеть упала");  // первый раз ошибка
    return ["товар A", "товар B"];
  });
  console.log("После 1-й загрузки:", await res.load());  // error
  console.log("После refresh:    ", await res.load());   // success
})();

Попробуй сам ▶ — первый вызов падает в error, повторный (как refresh) переводит ресурс в success. Так же ведут себя статусы в useFetch.

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

  • Игнорировать error. Необработанная ошибка оставит пользователя с пустым или сломанным экраном.
  • Не отдавать 404 при SSR. Без createError поисковик получит код 200 на несуществующую страницу — вред для SEO.
  • Перезапрашивать всё вручную. Реактивные параметры триггерят перезапрос сами — не дублируйте логику.

Best practices

  • Всегда ветвите шаблон по status/pending и error.
  • Некритичные данные грузите с lazy: true, чтобы не задерживать показ страницы.
  • Несуществующие сущности — через throw createError({ statusCode: 404 }) для корректного кода.

Итог: грамотная работа с данными — это не только data, но и состояния загрузки, ошибки и перезапрос. Nuxt даёт их из коробки. Откуда же берутся сами данные? Из серверных маршрутов — переходим к разделу про Nitro.

Проверьте себя
1. Что делает опция lazy: true в useFetch?
AКеширует данные навсегда
BНе блокирует навигацию ожиданием запроса — страница показывается сразу, а данные дольются
CОтключает серверный рендеринг
DЗапрещает перезапрос данных
2. Зачем при SSR использовать createError со statusCode 404 для несуществующей сущности?
AЧтобы ускорить запрос
BЧтобы Nuxt отдал страницу ошибки с корректным HTTP-кодом — иначе поисковик получит 200 на пустую страницу
CcreateError нужен только в dev-режиме
DЧтобы перезапросить данные