useAsyncData: гибкая загрузка

useAsyncData принимает любую async-функцию, а не только URL: для нескольких источников, своих SDK и преобразования ответа — это правильный выбор.
Суть: useAsyncData(key, fn) выполняет произвольную асинхронную функцию при SSR с кешированием по ключу. В отличие от useFetch, не привязан к одному URL: подходит для комбинации источников, кастомных клиентов и трансформации данных перед попаданием в компонент.

useFetch прекрасен для простого случая «один GET, один URL». Но реальность сложнее: иногда нужно дёрнуть два эндпоинта и слить результат, иногда — использовать SDK (например, клиент CMS), иногда — преобразовать ответ перед показом. Для этого есть useAsyncData — более общий инструмент.

Он принимает ключ и асинхронную функцию. Функция может делать что угодно — главное, вернуть данные:

<script setup>
const { data } = await useAsyncData("dashboard", async () => {
  const [user, stats] = await Promise.all([
    $fetch("/api/user"),
    $fetch("/api/stats"),
  ])
  return { user, stats }   // объединённый результат
})
</script>

Здесь мы делаем два запроса параллельно и возвращаем объединённый объект — useFetch так не умеет, потому что он про один URL. По сути useFetch(url) — это сокращение для useAsyncData(url, () => $fetch(url)). Знание этого снимает путаницу: useFetch — частный случай useAsyncData.

Полезная опция — transform: преобразовать ответ до того, как он попадёт в data и payload. Это уменьшает размер payload и упрощает шаблон.

   Когда что брать

   один GET, один URL          -> useFetch
   несколько источников/слияние -> useAsyncData
   свой SDK / не-HTTP операция  -> useAsyncData
   нужно трансформировать ответ -> transform в любом из них

Стоит держать в голове и обратную сторону SSR-загрузки: данные, попавшие в payload, увеличивают размер HTML-страницы. Если вы тянете огромный массив, а показываете из него лишь пару полей, весь массив поедет к клиенту в составе страницы. Поэтому полезно забирать с сервера ровно то, что нужно для рендера, а тяжёлую фильтрацию и обрезку делать ещё на стороне API или через опцию transform. Это держит payload компактным, а первый экран — лёгким, что напрямую влияет на скорость загрузки на слабых устройствах.

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

useAsyncData оборачивает вашу функцию в SSR-совместимый жизненный цикл: выполняет на сервере, кладёт результат в payload по ключу, на клиенте находит его и не выполняет функцию повторно. Ключ обязателен (в useFetch он выводится из URL автоматически) — именно по нему связываются серверный и клиентский результаты и работает дедупликация. В Nuxt 4 данные по умолчанию хранятся как shallowRef для производительности на вложенных объектах.

Смоделируем слияние нескольких источников и кеш по ключу — то, чем useAsyncData отличается от useFetch:

// useAsyncData: любая async-функция + кеш по ключу.
const cache = {};

async function api(name, value) {
  return { [name]: value };            // имитация источника
}

async function useAsyncData(key, fn) {
  if (cache[key]) return { data: cache[key], cached: true };
  const data = await fn();             // выполняем произвольную логику
  cache[key] = data;                   // кешируем по ключу
  return { data, cached: false };
}

(async () => {
  const first = await useAsyncData("dashboard", async () => {
    const [user, stats] = await Promise.all([
      api("user", "Алиса"),
      api("stats", 42),
    ]);
    return { ...user, ...stats };      // СЛИЯНИЕ источников
  });
  console.log("Первый вызов:", first);

  const second = await useAsyncData("dashboard", async () => ({}));
  console.log("Повтор по ключу:", second);   // из кеша, функция не звалась
})();

Попробуй сам ▶ — функция слила два источника в один объект, а повторный вызов по тому же ключу вернул кешированное без новой работы.

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

  • Забыть уникальный ключ. Без него Nuxt не свяжет сервер и клиент и может задублировать запрос.
  • Делать в setup то, что не нужно при SSR. Тяжёлую клиентскую логику выносите за useAsyncData.
  • Возвращать несериализуемое. Результат идёт в payload — функции и классы туда не положишь.

Best practices

  • Несколько источников или своя логика — useAsyncData; один URL — useFetch.
  • Используйте transform, чтобы не тащить в payload лишние поля ответа.
  • Задавайте осмысленные уникальные ключи — это и кеш, и дедупликация.

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

Проверьте себя
1. Когда useAsyncData предпочтительнее useFetch?
AВсегда, useFetch устарел
BКогда нужно объединить несколько источников, использовать SDK или преобразовать данные — то есть это не один простой GET-запрос
CТолько для статических сайтов
DКогда не нужен SSR
2. Зачем useAsyncData требует ключ?
AДля шифрования данных
BЧтобы связать серверный и клиентский результаты, кешировать и дедуплицировать запросы
CКлюч необязателен и ни на что не влияет
DЧтобы задать URL запроса