3. modulVue-fejjel Nuxtba: mi változik
Nuxt for Devs · 3. modul

Vue-fejjel Nuxtba: mi változik

A Vue-tudásod 90%-a változatlanul érvényes. Ez a modul a maradék 10%-ról szól — hét pontról, ahol a megszokott reflexed rossz kódot ír. Sorrendben a leggyakoribb és a legdrágább hibáktól haladunk a kellemetlenségek felé.

3.1Auto-import: mi jön magától, és mikor harap vissza

Nuxtban nem írsz importot a saját komponenseidhez, composable-jaidhoz, sem a Vue és a Nuxt API-jaihoz:

// Vue-projektben így írnád:
import { ref, computed, onMounted } from 'vue'
import UserCard from '~/components/UserCard.vue'
import { useCart } from '~/composables/useCart'

// Nuxtban egyik sem kell — mindhárom automatikusan elérhető.

Mi importálódik automatikusan, és mi nem:

Automatikusan elérhetőKézzel kell importálni
app/components/** (mélyen is, névtérrel)
app/composables/* és app/composables/*/index.ts
app/utils/* (ugyanezzel a szabállyal)
server/utils/* — csak szerveroldalon
shared/utils/*, shared/types/* — mindkét oldalon
Vue API-k: ref, computed, watch, lifecycle hookok
Nuxt API-k: useFetch, useState, navigateTo, …
Bármi a node_modules-ból (zod, drizzle-orm, …)
Beágyazott almappák a composables/ alatt (nem top-level)
A shared/ egyéb részei — ezekhez a #shared/… alias
Típusok, ha nem a shared/types/-ban vannak

A komponensek névtérbe kerülnek a mappaszerkezet szerint, ami elsőre meglepő:

app/components/UserCard.vue              →  <UserCard />
app/components/admin/UserCard.vue        →  <AdminUserCard />     ← nem <UserCard />!
app/components/admin/billing/Table.vue   →  <AdminBillingTable />

Ahol visszaharap

Kikapcsolható? Igen, az imports.autoImport: false beállítással — és nagy csapatoknál van, aki így dönt, a kiszámíthatóság kedvéért. Ha ezt teszed, cserébe minden Nuxt-composable-t is importálnod kell (import { useFetch } from '#app'). A gyakorlatban a legtöbben meghagyják, és inkább a névütközések ellen fegyelmeznek.

3.2useState vs. ref — a legdrágább hiba

Ez a szakasz ér a legtöbbet az egész modulban. Vue SPA-ban teljesen bevett minta a modul-szintű reaktív állapot:

✗ Nuxtban ez adatszivárgás
// app/composables/useCart.ts
const items = ref<Item[]>([])          // ← modul-scope!

export const useCart = () => {
  return { items, add: (i: Item) => items.value.push(i) }
}

A böngészőben ez tökéletes: egy felhasználó, egy modulpéldány. A szerveren viszont a modul-scope nem kérésenkénti. A modul egyszer értékelődik ki, és az összes kérés ugyanazt a items tömböt látja. Az egyik felhasználó kosara megjelenik a másiknál — vagy ami rosszabb, az egyik tenant adata a másiknál.

És miért nem veszed észre? Mert fejlesztés közben egyedül vagy a szerveren. Élesben, több párhuzamos kérésnél robban — Workers-en pedig különösen, mert az izolátumok újrahasznosulnak a kérések között. Ez a klasszikus cross-request state pollution, és a Cloudflare hivatalos best practices listáján is szerepel („avoid global mutable state”). A 6. modul kivesézi; itt csak annyi a lényeg, hogy ne írj modul-szintű ref-et.
✓ Így kell
// app/composables/useCart.ts
export const useCart = () => {
  // kérésenként izolált a szerveren, és átszerializálódik a kliensre
  const items = useState<Item[]>('cart', () => [])
  return { items, add: (i: Item) => items.value.push(i) }
}

A useState egy kulccsal azonosított, SSR-biztos állapot: a szerveren kérésenként külön példány, és az értéke bekerül a HTML payloadjába, hogy a kliens ugyanonnan folytassa. A szabály egyszerű:

Használd eztMikor
ref() komponensen belülLokális UI-állapot: nyitva van-e a modál, mit gépelt be a felhasználó. Ez teljesen rendben van.
useState('kulcs')Komponensek közt megosztott állapot, ami az SSR-t is túléli: bejelentkezett felhasználó, aktuális tenant, téma.
PiniaHa tényleg összetett store-logikád van (actionök, getterek, devtools-nyomkövetés). SSR-rel működik, de nem az első választás.
modul-szintű refSoha. Legalábbis olyasmire, ami felhasználóhoz köthető.
Ugyanez a szabály a server/ oldalon is él, sőt ott még élesebb: egy modul-szintű let currentUser vagy egy megosztott, kérésenként átírt objektum a server/utils/-ban pontosan ugyanezt a szivárgást csinálja. Amit a szerverkódban modul-szinten tarthatsz: immutábilis konfiguráció és felkészített kliensek, amiket nem írsz felül kérésenként.

3.3Composable-ok: hol hívhatók, és hol nem

A Nuxt-composable-ok (useRoute, useState, useFetch, useRuntimeConfig, …) egy rejtett kontextusból, a Nuxt-példányból dolgoznak. Ez a kontextus szinkron módon elérhető három helyen: komponens setup, plugin, és route middleware. Máshol nem.

✗ Tipikus elrontás
<script setup>
const { data } = await useFetch('/api/me')

// await UTÁN, callbackben, vagy eseménykezelőben:
button.addEventListener('click', async () => {
  const cfg = useRuntimeConfig()   // „Nuxt instance unavailable"
})
</script>
✓ Két megoldás
<script setup>
// ① a legegyszerűbb: hívd a tetején, szinkronban, és zárd be a változóba
const cfg     = useRuntimeConfig()
const nuxtApp = useNuxtApp()
const { data } = await useFetch('/api/me')

async function onClick() {
  console.log(cfg.public.apiBase)        // már a kezünkben van

  // ② ha tényleg kontextus kell egy async ág mélyén:
  await nuxtApp.runWithContext(() => navigateTo('/projects'))
}
</script>

A gyakorlati szabály: minden use* hívás a <script setup> tetejére, szinkronban, az await-ek elé. Ha ezt megtartod, ezzel a hibaosztállyal nem találkozol.

Saját composable írása ugyanígy működik — csak arra figyelj, hogy ha Nuxt-composable-t hívsz benne, akkor a te composable-od is örökli a megkötést:

// app/composables/useTenant.ts
export const useTenant = () => {
  const route  = useRoute()                          // örökli a setup-megkötést
  const tenant = useState<Tenant | null>('tenant', () => null)
  const slug   = computed(() => route.params.tenant as string)
  return { tenant, slug }
}

3.4Compiler-makrók: definePageMeta és társai

Néhány Nuxt-függvény nem futásidejű hívás, hanem build-time makró — a fordító kiemeli a kódból, és a route-táblába fordítja. Ezért nem lehet nekik dinamikus értéket adni:

<script setup>
// ✓ így jó — statikus objektum
definePageMeta({
  layout: 'admin',
  middleware: ['auth', 'tenant'],
})

// ✗ így nem — a makró nem lát futásidejű változót
const l = user.isAdmin ? 'admin' : 'default'
definePageMeta({ layout: l })      // build-hiba vagy néma hiba

// ✓ ha futásidőben kell váltani:
setPageLayout(user.isAdmin ? 'admin' : 'default')
</script>

Ugyanez a logika érvényes a defineNuxtRouteMiddleware-re és a defineNuxtPlugin-re: a fájl helye és a makró együtt mondja meg a Nuxtnak, mit csináljon a kóddal.

3.5.client, .server és a feltételes futás

Négy eszközöd van arra, hogy valami csak az egyik oldalon fusson. Ezek nem cserélhetők fel:

EszközMit csinálMikor
MapView.client.vueA komponens SSR-kor egyáltalán nem renderelődik; csak a böngészőben.Az egész komponens böngésző-függő: térkép, chart, WYSIWYG-szerkesztő.
<ClientOnly>A benne lévő rész csak kliensen renderel — de adhatsz #fallback-et az SSR-hez.Egy komponens része problémás, és kell szerveroldali helykitöltő.
plugins/x.client.tsA plugin csak a böngészőben fut le.Analitika, hibakövető SDK, böngésző-API-k inicializálása.
onMounted()Sosem fut a szerveren — ez a garantáltan kliensoldali életciklus-hook.Egy sornyi böngésző-hozzáférés egy egyébként SSR-elt komponensben.
<template>
  <ClientOnly>
    <RevenueChart :data="data" />
    <template #fallback>
      <!-- ez megy ki a HTML-be SSR-kor: nincs layout-ugrás, van mit indexelni -->
      <div class="chart-skeleton">Diagram betöltése…</div>
    </template>
  </ClientOnly>
</template>
A <ClientOnly> nem javítás, hanem kompromisszum. Amit belecsomagolsz, az kikerül az SSR-ből: nem lesz benne a HTML-ben, a robot nem látja, és a felhasználó később látja meg. Ha hydration mismatch elől menekülsz vele, azzal a tünetet takarod el. Először mindig azt kérdezd meg, miért tér el a szerver és a kliens kimenete — a következő szakasz erről szól.

3.6Hydration mismatch: a hét ok

Ez az a hibaosztály, ami SPA-ban nem létezik, és Nuxtban a leggyakoribb „miért csinálja ezt?” pillanat. A hiba mindig ugyanaz: a szerveren rendereltől eltérő HTML-t akar a kliens. A konzolban valami ilyet látsz: Hydration node mismatch vagy Hydration text content mismatch, és a komponens újrarenderelődik a kliensen (villan).

OkPéldaJavítás
Idő és dátum {{ new Date().toLocaleString() }} — a szerveren más másodperc, más időzóna Rögzített időpontot adj át (a szerveren keletkezett), és a relatív időt („3 perce”) számold onMounted után
Véletlen Math.random(), generált ID useId() a stabil azonosítókhoz; egyébként kliensre halasztani
Böngésző-API elágazás v-if="window.innerWidth < 768" SSR-kor egy alapértelmezés, majd onMounted-ben pontosítás — vagy CSS media query, ami nem is JS
localStorage-ból olvasott állapot téma, nyelv, „elrejtettem ezt a bannert” Cookie-ba tedd (useCookie) — azt a szerver is látja, tehát nem tér el a két kimenet
Érvénytelen HTML-beágyazás <div> egy <p>-ben, <a> egy <a>-ban A böngésző „megjavítja” a HTML-t, ezért lesz más a fa. Javítsd a markupot — ez mindig a te hibád
Nem determinisztikus adat rendezetlen lista, Object.keys() sorrendje, „random ajánlás” Rendezz explicit módon, és a szerveri választ használd a kliensen is
Harmadik fél belenyúl a DOM-ba böngésző-kiterjesztés, chat-widget, fordító Nem tudod javítani — de fel tudod ismerni: inkognitóban, kiterjesztések nélkül eltűnik

Hogyan szűkítsd le

  1. Nézd meg a nyers HTML-t. curl vagy „forrás megtekintése” (nem a DevTools Elements fül, mert az már a hidratált DOM-ot mutatja). Ha ott már rossz, akkor SSR-hiba, nem hydration-hiba.
  2. A konzolüzenet megmondja a csomópontot. Dev módban a Vue kiírja, melyik komponensben és milyen tartalomnál tér el.
  3. Kapcsold ki felezéssel. Ha nagy az oldal, tedd <ClientOnly>-ba a gyanús felet — nem javításként, hanem hogy megtaláld a bűnöst.
  4. Inkognitó, kiterjesztések nélkül. Ha ott nem jelentkezik, nem a te kódod a hibás.
Nuxt 4.5-től a hibáknak stabil, gépi olvasható kódjuk van (NUXT_E… formában), ami nagyban gyorsítja a keresést — a hibaüzenetre rákeresve célzott dokumentációt kapsz, nem StackOverflow-régészetet. A 16. modulban a hibakeresést részletesen is végigvesszük.

3.7Idióma-váltó: amit másképp fogsz írni

Ezek nem hibák, csak más szokások. A bal oldali is működik általában — a jobb oldali az, ami Nuxtban helyes, és amit SSR mellett elvárnak tőled:

Vue-ban ígyNuxtban ígyMiért
<RouterLink to><NuxtLink to>Automatikus prefetch, külső linkek kezelése, prerender-tudatosság
router.push('/x')navigateTo('/x')Szerveren is működik — ott valódi HTTP-átirányítást csinál, nem kliensoldali ugrást
document.title = xuseSeoMeta({ title: x })A meta már a szerveroldali HTML-be kerül — ez az SSR fél haszna
import.meta.env.VITE_APIuseRuntimeConfig()Futásidőben állítható, és a titkok nem kerülnek a kliens-bundle-be (12. modul)
axios-példány$fetch / useFetchA szerveren a belső /api/… hívás közvetlen függvényhívássá válik — nincs HTTP-kör, Workers-en nincs extra subrequest
main.ts + app.use()app/plugins/x.tsNincs saját belépési pont; a plugin-mappa veszi át a szerepét
App.vue + <RouterView>app/app.vue + <NuxtLayout><NuxtPage/>A layout-réteg beépült a routingba
Pinia mindenreuseState() az egyszerű esetekreKevesebb függőség, és SSR-biztos alapból
Egy Workers-specifikus nyereség a táblából: amikor a szerveroldali SSR alatt $fetch('/api/projects')-et hívsz, a Nitro nem indít HTTP-kérést önmaga felé, hanem közvetlenül meghívja a handlert. Workers-en ez kifejezetten értékes: nem fogyaszt subrequestet, nem terheli a 6 párhuzamos kimenő kapcsolat korlátját, és nincs hálózati késleltetés. Külső URL-nél viszont valódi kérés megy ki — ott már számít.

3.8Az első hét ellenőrzőlistája

Ha most kezdenél átírni valamit Nuxtra, ezt a hatot nézd át magadon:

Mi jön ezután? Innentől kezdünk építeni. A 4. modul a routingról, a layoutokról és a middleware-ekről szól — beleértve azt is, hogyan tervezd meg a multitenant útvonalaidat (/t/:tenant/… vs. subdomain vs. egyedi domain), mert ez a döntés később nehezen visszafordítható.

3.9Ellenőrizd magad

  1. Miért veszélyes egy const user = ref(null) a composable-fájl tetején, modul-szinten?
    Válasz

    Mert a szerveren a modul-scope nem kérésenkénti: a modul egyszer értékelődik ki, és minden kérés ugyanazt a ref-et látja. Így az egyik felhasználó (vagy tenant) adata megjelenhet a másiknál. Workers-en ez különösen valószínű, mert az izolátumok újrahasznosulnak. Megoldás: useState('user', () => null), ami kérésenként izolált és a payloadon keresztül átjut a kliensre.

  2. Mi a szabály arra, hogy hol hívhatsz Nuxt-composable-t?
    Válasz

    Szinkron módon, komponens-setupban, pluginben vagy route middleware-ben. Gyakorlati megfogalmazás: a <script setup> tetején, az await-ek elé. Ha egy async ág mélyén mégis kell a kontextus, akkor useNuxtApp().runWithContext(() => …).

  3. Van egy app/components/admin/UserCard.vue-d. Hogy hivatkozol rá a sablonban?
    Válasz

    <AdminUserCard /> — a mappaszerkezet névtérként előtagolja a komponens nevét. Ha ez zavar, a components konfigban kikapcsolható a pathPrefix, de akkor ügyelj a névütközésekre.

  4. A felhasználó által választott téma localStorage-ban van, és hydration mismatch-et okoz. Mi a jó megoldás, és mi a rossz?
    Válasz

    Rossz: <ClientOnly>-ba csomagolni az egész oldalt — ezzel elveszted az SSR-t. Jó: cookie-ba tenni a témát (useCookie), mert azt a szerver is látja a kérésből, így a szerveroldali és a kliensoldali render ugyanazt adja. Ez a minta minden „felhasználói beállítás, ami befolyásolja az első renderelést” esetre igaz.

  5. Miért nem adhatsz változót a definePageMeta-nak?
    Válasz

    Mert nem futásidejű függvényhívás, hanem build-time compiler-makró: a fordító kiemeli a komponensből, és a generált route-táblába fordítja, ahol futásidejű változó nem létezik. Ha futásidőben kell layoutot váltani, arra a setPageLayout() való.

  6. Mi történik Workers-en, ha SSR közben $fetch('/api/projects')-et hívsz?
    Válasz

    A Nitro felismeri, hogy belső útvonalról van szó, és közvetlenül meghívja a handlert — nem megy ki HTTP-kérés. Ez Workers-en konkrét nyereség: nem fogyaszt subrequestet, nem terheli a párhuzamos kimenő kapcsolatok korlátját, és nincs hálózati késleltetés. Külső URL-re viszont valódi kérés indul.

Előző2. modul — A Nuxt anatómiája: mi épül, és mi hol fut Következő 4. modul — Routing, layoutok, middleware