5. modulAdatlekérés — a kulcsmodul
Nuxt for Devs · 5. modul · kulcsmodul

Adatlekérés: useFetch, useAsyncData, $fetch

A tananyag legfontosabb modulja. Itt dől el, hogy az appod kétszer kérdezi-e le ugyanazt az adatot, hogy villog-e betöltéskor — és itt van elrejtve az a csapda, amivel más tenant adatát szivárogtathatod ki a HTML-be anélkül, hogy valaha kirendernéd.

5.1A három eszköz, és mire való mindegyik

Elsőre zavaró, hogy három dolog is „adatot kér le”. Valójában rétegek:

EszközMi ezSSR-tudatos?
$fetchA HTTP-kliens (az ofetch). Kér egyet, visszaad egy promise-t. Semmi több.Nem
useAsyncDataAz SSR-réteg: kulccsal ellátja az eredményt, átviszi a szerverről a kliensre, kezeli a betöltési állapotot és a dedupot. Bármilyen async függvényt elfogad, nem csak HTTP-t.Igen
useFetchA kettő egyben: useAsyncData + $fetch, automatikusan generált kulccsal. A napi eszközöd.Igen

A hiba, amit szinte mindenki elkövet

✗ Kétszer fut le
<script setup>
const projects = await $fetch('/api/projects')
</script>

Ez működni fog — és ettől a legrosszabb. Ami történik: a setup lefut a szerveren, lekéri az adatot, kirendereli a HTML-t. Aztán a böngészőben, a hydration alatt ugyanez a setup újra lefut, és mivel a $fetch-nek fogalma sincs a szerveren már megtörtént lekérésről, megint lekérdezi. Kétszeres adatbázis-terhelés, plusz egy fölösleges kör a böngészőből.

✓ Egyszer fut le, az eredmény átjön a HTML-lel
<script setup>
const { data: projects, status, error } = await useFetch('/api/projects')
</script>

Döntési szabály

HelyzetEzt használd
Az oldal adata, komponens setup-banuseFetch
Nem HTTP-forrás (SDK, binding, több hívás összefésülve)useAsyncData('kulcs', () => …)
Eseménykezelő: gombnyomás, űrlapküldés, törlés$fetch
server/api handleren belül$fetch (belső útvonalra közvetlen hívás lesz)
// eseménykezelőben $fetch — itt nincs SSR, nincs mit átvinni
async function archive(id: string) {
  await $fetch(`/api/projects/${id}/archive`, { method: 'POST' })
  await refresh()          // a useFetch-ből kapott refresh
}

5.2A kulcs — a legfontosabb paraméter, amiről nem beszélnek

Minden useAsyncData/useFetch hívásnak van egy kulcsa. Ez azonosítja az eredményt a payloadban, ezen keresztül talál rá a kliens a szerveren lekért adatra, és ez alapján osztozik két komponens ugyanazon az adaton.

A useFetch automatikusan generál kulcsot (a hívás helye és az URL alapján). Ez a legtöbbször jó — de amint az URL dinamikus, érdemes átvenni az irányítást:

<script setup lang="ts">
const route = useRoute()

// reaktív URL: ha az id változik, automatikusan újratölt (Nuxt 4: reaktív kulcsok)
const { data: project } = await useFetch(
  () => `/api/projects/${route.params.id}`,
  { key: () => `project:${route.params.id}` },
)
</script>

Két képesség, amit a kulcs ad:

Multitenant-figyelmeztetés. Ha a kulcs nem tartalmazza a tenantot, két különböző munkaterület adata ugyanarra a kulcsra kerül. Kliensoldali tenantváltásnál (ha az appodban van ilyen) ez azt jelenti, hogy a régi tenant adata jelenik meg az újban, amíg a frissítés le nem fut. A szabály egyszerű: a kulcsba tedd bele mindent, amitől a válasz függ — tenant, felhasználó, szűrők.
const { slug } = useTenant()
const filters = ref({ status: 'active' })

const { data: projects } = await useFetch('/api/projects', {
  query: filters,
  key: () => `projects:${slug.value}:${filters.value.status}`,
  watch: [slug, filters],
})

5.3Opció-referencia

Ezt a táblát fogod visszakeresni. A félkövérek azok, amiket hetente használsz:

OpcióMit csinálMikor
keyAz eredmény azonosítója. Lehet getter is (reaktív kulcs).Amint dinamikus az URL.
lazytrue esetén nem blokkolja a navigációt — az oldal azonnal megjelenik, az adat később érkezik.Másodlagos tartalomra (oldalsáv, statisztika). A fő tartalomra ne.
serverfalse esetén csak a kliensen fut — nem kerül a payloadba.Személyre szabott vagy bizalmas részekre, amit nem akarsz a HTML-ben.
watchReaktív források, amik változásra újratöltenek.Szűrők, lapozás, tenantváltás.
transformÁtalakítja az eredményt, mielőtt a payloadba kerül.Formázás és — fontos — a fölös mezők kidobása.
pickCsak a felsorolt kulcsokat tartja meg.Ugyanaz egyszerűbben, ha lapos az objektum.
enabledKapuzza a futást — ha false, nem indul el (4.5).„Csak akkor kérd le, ha már megvan a session.”
immediatefalse esetén csak kézi execute()-ra fut.Gombra induló lekérés.
defaultKezdőérték, amíg nincs adat.() => [], hogy ne kelljen v-if mindenhova.
dedupe'cancel' (alap) vagy 'defer' párhuzamos hívásoknál.Gyorsan gépelt keresőmezőnél.
getCachedDataSaját cache-logika: honnan vegye, ha már megvan.Navigáció közti megőrzés — óvatosan (lásd lentebb).

Visszatérési értékek

const {
  data,      // az eredmény (ref)
  status,    // 'idle' | 'pending' | 'success' | 'error'  ← ezt használd a sablonban
  error,     // a hiba, ha volt
  refresh,   // újratöltés
  execute,   // ugyanaz, immediate:false mellett használatos
  clear,     // data/error/status visszaállítása
} = await useFetch('/api/projects')
<template>
  <ProjectSkeleton v-if="status === 'pending'" />
  <ErrorBox v-else-if="status === 'error'" :error="error" />
  <ProjectList v-else :items="data" />
</template>

5.4Hibakezelés

A két eszköz máshogy viselkedik hibánál, és ez rendszeresen meglep:

// szerveroldalon így keletkezik a hiba:
throw createError({
  statusCode: 404,
  statusMessage: 'Nincs ilyen projekt',   // ← ezt LÁTJA a kliens
  data: { field: 'id' },                   // ← ez is átmegy, ha megadod
  message: 'projects.findFirst returned null for tenant 42', // ← ez NEM megy ki élesben
})
// kliensoldalon, eseménykezelőben:
try {
  await $fetch('/api/projects', { method: 'POST', body })
} catch (e: any) {
  if (e.statusCode === 422) formErrors.value = e.data?.errors
  else toast.error('Nem sikerült létrehozni')
}

Ha egy hiba az egész oldalt érvényteleníti (nincs ilyen projekt, nincs jogosultság), akkor fatal: true-val vagy showError()-ral az app/error.vue-ra dobod — ez a 4. modulban volt.

5.5A payload — a csapda, amiről kevesen tudnak

Amikor a useFetch a szerveren lefut, az eredményt valahogy át kell adni a kliensnek, hogy az ne kérje le újra. Ez a payload: a Nuxt beleírja az eredményt a HTML-be, egy <script id="__NUXT_DATA__"> elembe.

Nézd meg magad: nyiss meg egy Nuxt-oldalt, és a forrást nézd (nem a DevTools Elements fülét, mert az már a hidratált DOM). A HTML alján ott lesz az összes szerveroldali lekérésed eredménye — nyersen, JSON-szerű formában.

És itt a probléma: a payloadba az egész válasz bekerül, nem csak az, amit kirenderelsz. Ha a server route-od visszaadja a teljes adatbázis-sort, akkor a belso_megjegyzes, a beszerzesi_ar, a tulajdonos_email és a tenant_id is kimegy a böngészőbe — akkor is, ha a sablonban csak a nevet írod ki. Ez nem elméleti: ez az egyik leggyakoribb adatszivárgási forma SSR-es appokban.
✗ A teljes sor kimegy a HTML-be
// server/api/projects/[id].get.ts
export default defineEventHandler(async (event) => {
  return await useDb(event).query.projects.findFirst({
    where: eq(projects.id, getRouterParam(event, 'id')!),
    with: { owner: true },        // ← és a teljes user-sor is, jelszó-hash-sel együtt
  })
})
✓ Explicit DTO — csak ami tényleg kell
export default defineEventHandler(async (event) => {
  const { tenantId } = await requireSession(event)

  const p = await useDb(event).query.projects.findFirst({
    where: and(
      eq(projects.id, getRouterParam(event, 'id')!),
      eq(projects.tenantId, tenantId),      // a tenant-szűrés MINDIG a lekérdezésben
    ),
    with: { owner: { columns: { id: true, name: true, avatarUrl: true } } },
  })

  if (!p) throw createError({ statusCode: 404, statusMessage: 'Nincs ilyen projekt' })

  // és a válasz alakja is explicit — nem a DB-séma szivárog ki az API-ba
  return {
    id: p.id, name: p.name, status: p.status, updatedAt: p.updatedAt,
    owner: { id: p.owner.id, name: p.owner.name, avatarUrl: p.owner.avatarUrl },
  }
})

A második védvonal: pick és transform

Ezek a kliensoldali hívásnál szűkítik a payloadot — a szerveren futnak le, mielőtt a szerializálás megtörténik, tehát tényleg csökkentik, ami kimegy:

const { data } = await useFetch('/api/projects', {
  pick: ['id', 'name', 'status'],
})

// vagy összetettebb esetben:
const { data } = await useFetch('/api/projects', {
  transform: (rows) => rows.map(r => ({ id: r.id, name: r.name })),
})
De ez nem helyettesíti az elsőt. A pick/transform jó a payload-méret csökkentésére, de az adat így is átment a szerveri renderelőn, és ha bármikor server: false-ra váltasz vagy kliensről hívod ugyanazt a végpontot, a teljes objektum kimegy a dróton. A helyes szabály: a server route soha ne adjon vissza nyers adatbázis-sort. A pick a második védvonal, nem az első.

Amit ne engedj a payloadba

Ha valami tényleg csak a bejelentkezett felhasználónak szól és nem kell SSR-ben, a server: false a helyes eszköz — akkor a lekérés kizárólag a böngészőben történik, és semmi nem kerül a HTML-be:

// személyre szabott, nem indexelendő, nem cache-elhető rész
const { data: notifications } = await useFetch('/api/me/notifications', {
  server: false,
  lazy: true,
})
Ennek Cloudflare-vonzata is van. Ha az oldal HTML-je tartalmaz felhasználó-specifikus payloadot, akkor azt a HTML-t soha nem cache-elheted — mert a következő látogató a te adatodat kapná. Ha viszont a személyes részt server: false-szal kliensre teszed, a váz cache-elhetővé válik. Ez a 18. modul fő gondolata; itt csak annyi, hogy a cache-elhetőség már ebben a modulban eldől.

5.6Vízesés és párhuzamosítás

A soros await-ek vízesést csinálnak: a második lekérés csak az első befejeződése után indul el.

✗ Vízesés — az összeadódó latency
const { data: user }     = await useFetch('/api/me')
const { data: projects } = await useFetch('/api/projects')
const { data: usage }    = await useFetch('/api/usage')
✓ Párhuzamosan
const [{ data: user }, { data: projects }, { data: usage }] = await Promise.all([
  useFetch('/api/me'),
  useFetch('/api/projects'),
  useFetch('/api/usage'),
])
✓✓ Még jobb: egy végpont, ami mindent összeszed
// server/api/dashboard.get.ts — egy adatbázis-menet, egy válasz
export default defineEventHandler(async (event) => {
  const { userId, tenantId } = await requireSession(event)
  const db = useDb(event)

  const [projects, usage] = await Promise.all([
    db.query.projects.findMany({ where: eq(projects.tenantId, tenantId), limit: 10 }),
    getUsage(db, tenantId),
  ])

  return { projects: projects.map(toProjectDto), usage }
})
Workers-szempont. Az SSR alatti useFetch('/api/…') belső hívás — a Nitro közvetlenül meghívja a handlert, nem indít HTTP-kérést (3. modul). Tehát a fenti három hívás nem fogyaszt subrequestet. Ami viszont igen számít: a Workers-runtime egyszerre 6 kimenő kapcsolatot tart nyitva, tehát ha a server route-jaid külső API-kat hívnak, ott a párhuzamosításnak van felső határa. És a CPU-idő is mérve van: a vízeséstől nem lesz több CPU-fogyasztás, de a válaszidő igen — a felhasználó azt érzi meg.

5.7getCachedData — hasznos, de vigyázz vele

Alapból a useAsyncData minden navigációnál újratölt. A getCachedData-val megmondhatod, hogy ha már megvan az adat, ne kérje le újra:

const { data } = await useFetch('/api/settings', {
  key: () => `settings:${slug.value}`,
  getCachedData: (key, nuxtApp, ctx) => {
    // kézi frissítéskor mindig menjen új kérés
    if (ctx.cause === 'refresh:manual') return undefined
    return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
  },
})
Multitenant csapda ismét: ha a getCachedData kulcsa nem tartalmazza a tenantot (és a felhasználót), akkor a cache-ed a tenant-határon átnyúlik. A gyakorlati szabály ugyanaz, mint a kulcsnál: ami befolyásolja a választ, az legyen a kulcsban. Ha bizonytalan vagy, ne használd — az újratöltés olcsóbb, mint egy adatszivárgás.

5.8Ellenőrzőlista egy adatot lekérő oldalhoz

Mi jön ezután? A 6. modul az állapotkezelésről szól: a useState részletesen, mikor kell tényleg Pinia, és a cross-request state pollution teljes anatómiája — az a hiba, amit a 3. modulban már felvillantottunk, de amit érdemes egyszer rendesen megérteni, mert Workers-en különösen könnyű elkövetni.

5.9Ellenőrizd magad

  1. Miért rossz a const data = await $fetch('/api/x') egy komponens setup-jában?
    Válasz

    Mert a $fetch nem SSR-tudatos: nincs kulcsa, és az eredménye nem kerül a payloadba. A setup lefut a szerveren (lekérés #1), majd a hydration alatt a böngészőben újra (lekérés #2). Dupla adatbázis-terhelés és egy fölösleges hálózati kör. Helyette useFetch.

  2. Mi a payload, és hogyan nézed meg?
    Válasz

    A szerveroldali useAsyncData/useFetch eredményeinek szerializált másolata, amit a Nuxt beleír a HTML-be egy <script id="__NUXT_DATA__"> elembe, hogy a kliensnek ne kelljen újra lekérnie. Megnézni a lap forrásában lehet (nem a DevTools Elements fülén, mert az már a hidratált DOM-ot mutatja).

  3. A sablonban csak a projekt nevét írod ki. Miért kerülhet mégis a böngészőbe a beszerzési ár?
    Válasz

    Mert a payloadba a server route teljes válasza kerül, nem az, amit kirenderelsz. Ha a végpont a nyers adatbázis-sort adja vissza, minden mezője kimegy a HTML-be. Megoldás: a server route explicit DTO-t adjon vissza; másodlagosan pick/transform a lekérésnél.

  4. Két oldalad ugyanazt az adatot mutatja, és látod, hogy kétszer kérdezi le. Mit állítasz be?
    Válasz

    Ugyanazt a key-t mindkét helyen — a Nuxt így megosztja az eredményt, és csak egyszer kér le. Máshonnan az adat useNuxtData('kulcs')-csal is elérhető, célzottan frissíteni pedig refreshNuxtData('kulcs')-csal lehet.

  5. Melyik lekérésnél használnál server: false-t, és mit nyersz vele?
    Válasz

    Személyre szabott, nem indexelendő tartalomnál — értesítések, „legutóbb megnyitott”, felhasználó-specifikus ajánlások. Két dolgot nyersz: nem kerül a HTML payloadjába (nincs szivárgás), és a HTML váza cache-elhetővé válik, mert nincs benne felhasználó-specifikus adat.

  6. Miért nem elég a tenant-szűrést a válaszon elvégezni (rows.filter(r => r.tenantId === t))?
    Válasz

    Mert az adat így már kijött az adatbázisból: fölösleges terhelés, és egyetlen elfelejtett szűrés az egész más tenant adatát kiadja. A tenant-feltétel a lekérdezés WHERE-ágába való, minden egyes lekérdezésben. A szűrés a válaszon nem védelem, csak kozmetika.

Előző4. modul — Routing, layoutok, middleware Következő 6. modul — Állapotkezelés és SSR-csapdák