Először egy kérdés, ami a legtöbb állapotkezelési problémát megelőzi: hova tartozik ez az adat? Utána a useState, a Pinia mérlege, a cookie- és URL-alapú állapot — és a modul közepén a cross-request state pollution teljes anatómiája, mert Workers-en ez a legkönnyebben elkövethető, legnehezebben észrevehető hiba.
Vue SPA-ban egy helyre szokott kerülni minden: a store-ba. SSR mellett ez pazarlás és hibaforrás egyszerre, mert sok „állapot” valójában nem állapot. Mielőtt bármit useState-be tennél, fusd át ezt:
| Ha ez… | …akkor ide tartozik | Miért |
|---|---|---|
| Csak egy komponensen belül számít (modál nyitva, gépelt szöveg) | ref() a komponensben | Nem kell megosztani, nem kell túlélnie semmit. |
| A szerverről jön, és a szerver az igazság forrása (projektek, felhasználó adatai) | useFetch / useAsyncData | Ez nem állapot, hanem gyorsítótárazott szerveri adat. Kulcsa van, frissíthető (5. modul). |
| Megosztható és a vissza-gombnak működnie kell (szűrők, lapozás, rendezés) | URL query-paraméter | Megosztható link, működő előre/vissza, SSR-ben azonnal helyes. |
| A szervernek is látnia kell az első renderelésnél (téma, nyelv) | useCookie | A cookie a kéréssel utazik, tehát az SSR is látja — nincs hydration mismatch. |
| Bizalmas vagy hitelesítéshez tartozik (session) | Szerveroldali session + httpOnly cookie | A kliens nem is olvashatja. (10. modul) |
| Több komponens osztozik rajta, és túl kell élnie az SSR-t (bejelentkezett felhasználó, aktuális tenant) | useState('kulcs') | Ez az, ami tényleg globális kliensállapot. |
useState-ben tartod, elveszted a megosztható linket és a vissza-gombot. Ha a szerveri listát useState-be másolod, két igazságforrásod lesz, és neked kell szinkronban tartanod őket. Kérdezd meg minden adatnál: mi az igazság forrása?useState a gyakorlatban// app/composables/useCurrentUser.ts
export const useCurrentUser = () => {
// az initializer CSAK akkor fut, ha ehhez a kulcshoz még nincs érték
return useState<User | null>('auth:user', () => null)
}
Három dolog, amit érdemes tudni róla:
'user' kulcs, ugyanazt az állapotot használják. Prefixálj: 'auth:user', 'ui:sidebar', 'tenant:current'.Date, Map, Set, BigInt rendben van — függvény és osztálypéldány nem. Ha osztályt teszel bele, a kliensen sima objektumként ébred fel, metódusok nélkül.useState-be teszel a szerveren, azt a felhasználó látja a HTML forrásában. Ne tegyél bele belső mezőt.// „store" Nuxt-módra — nem kell hozzá könyvtár
export const useSidebar = () => {
const open = useState('ui:sidebar', () => true)
const toggle = () => { open.value = !open.value }
return { open, toggle }
}
// takarítás — például kijelentkezéskor vagy tenantváltásnál
clearNuxtState(['auth:user', 'tenant:current'])
Ez a modul lényege. A 3. modulban már felvillantottuk; most nézzük meg, miért történik, és hol bújik el.
Mert a hiba eltűnik, amikor keresed. Fejlesztés közben egyedül vagy: egy kérés, egy állapot, minden rendben. Az első éles napokon is működhet, ha kevés a forgalom, és az izolátum kérések között „kipiheni magát”. Aztán jön egy nap, amikor két felhasználó egyszerre kattint, és valaki más adatait látja.
// app/composables/useFilters.ts
const filters = reactive({ status: 'active' }) // ✗ minden kérés ugyanezt írja
export const useFilters = () => filters
server/utils-ban// server/utils/context.ts
let currentTenant: Tenant | null = null // ✗ a legveszélyesebb változat
export function setTenant(t: Tenant) { currentTenant = t }
export function getTenant() { return currentTenant }
Ez a második a legrosszabb, mert kényelmesnek tűnik („ne kelljen mindenhova átadni az event-et”), és mert szerveroldalon van, ahol a valódi adat lakik. Két párhuzamos kérésnél az egyik felülírja a másik tenantját, és a lekérdezések a rossz tenanttal futnak le. A helyes megoldás a 4. modulból: event.context.tenant, és az event-et végig átadod.
// server/utils/settings.ts
let cached: Settings | null = null
export async function getSettings(event) {
if (!cached) cached = await loadSettings(event) // ✗ melyik tenanté?!
return cached
}
A cache önmagában nem baj — de a kulcsának tartalmaznia kell mindent, amitől az érték függ. Ha tenantonként más, akkor Map<tenantId, Settings> kell, és még akkor is korlátos élettartammal. A jó megoldás általában nem is memóriában van: KV vagy a Nitro cache-rétege, tenantra kulcsolva (18. modul).
| Szabad | Nem szabad |
|---|---|
|
Konstansok, enumok, séma-definíciók Tiszta függvények Előre lefordított regexek, validátorok Immutábilis konfigurációs objektumok |
Bármi, ami felhasználóhoz vagy tenanthoz köthető Kérésenként átírt objektum vagy tömb „Aktuális” bármi ( currentUser, currentTenant)Kulcs nélküli cache |
env-en keresztül. Vagyis a platform fizikailag megakadályozza, hogy modul-szinten építs adatbázis-klienst; helyette kérésenként kell felépítened az event-ből. Ez elsőre kényelmetlen, valójában viszont pont a helyes irányba terel. A 11. modulban megcsináljuk a wrappert, ami ezt elrejti.server/ alatt és a composables/-ben ne legyen olyan let, reactive() vagy ref() a fájl tetején (export-függvényen kívül), ami nem konstans. Ezt code review-ban kell nézni — lintszabállyal is kikényszeríthető.Promise.all két $fetch-csel már kimutatja a durva eseteket.A Pinia jó könyvtár, és a @pinia/nuxt modullal SSR-biztos: a store-ok kérésenként példányosulnak, az állapot a payloadon keresztül jut a kliensre. Nem kell félni tőle — csak nem az első választás.
Maradj useState-nél, ha… | Válts Piniára, ha… |
|---|---|
|
Néhány globális érték van (felhasználó, tenant, téma) Az adat többsége úgyis useFetch-ből jönNem kell nyomkövetni az állapot változásait |
Összetett domain-logika sok actionnel és getterrel Store-ok épülnek egymásra Kell a devtools időutazás és az akciónapló A csapat már ismeri, és van rá kialakult mintátok |
defineStore kívül hozol létre reaktív értéket, és onnan hivatkozol rá, akkor pontosan ugyanazt a szivárgást csinálod, mint a 6.3-ban. A Pinia attól SSR-biztos, hogy a store-példányokat a Nuxt-plugin kérésenként hozza létre — nem attól, hogy Piniának hívják. És ugyanaz a payload-szabály is él rá: amit a store-ban tartasz a szerveren, az kimegy a HTML-be.useCookieEmlékszel a 3. modul hydration-táblájából a localStorage-sorra? Ez a megoldás rá. A cookie a kéréssel együtt utazik, tehát a szerver már az első rendereléskor látja — nincs eltérés a szerveri és a kliensi kimenet között:
// app/composables/useTheme.ts
export const useTheme = () => {
return useCookie<'light' | 'dark'>('theme', {
default: () => 'light',
maxAge: 60 * 60 * 24 * 365,
sameSite: 'lax',
secure: true,
path: '/',
})
}
// használat — reaktív ref, írásra a böngésző cookie-ját is frissíti
const theme = useTheme()
theme.value = 'dark'
Amire figyelj:
httpOnly cookie-t a kliensen nem tudsz olvasni — ez a session helyes beállítása, de akkor a useCookie csak szerveroldalon ad értéket. Ez szándékos: a session ne legyen JS-ből elérhető.domain beállítás dönt. Ha aldomain-modellben a szülő-domainre sütöd, minden tenant aldomainje látja (4. modul). Ez lehet szándékos (közös bejelentkezés), de nem lehet véletlen.Szűrők, rendezés, lapozás, kijelölt fül: ezek majdnem mindig az URL-be valók, nem useState-be. Négy dolgot kapsz ingyen: megosztható linket, működő vissza-gombot, azonnal helyes SSR-t, és cache-elhetőséget.
<script setup lang="ts">
const route = useRoute()
const router = useRouter()
// az URL az igazság forrása
const status = computed({
get: () => (route.query.status as string) ?? 'active',
set: (v) => router.replace({ query: { ...route.query, status: v, page: undefined } }),
})
// és a lekérés egyszerűen követi
const { data: projects } = await useFetch('/api/projects', {
query: { status },
key: () => `projects:${slug.value}:${status.value}`,
})
</script>
ref.Ha az appodban lehet tenantot váltani anélkül, hogy teljes oldalújratöltés történne, akkor a váltás egy invalidálási esemény: minden tenanthoz kötött állapotot és lekért adatot el kell dobni.
export async function switchTenant(slug: string) {
// ① minden tenanthoz kötött kliensállapot törlése
clearNuxtState(['tenant:current', 'ui:recent', 'billing:summary'])
// ② a lekért adatok eldobása — ha a kulcsaid tenant-prefixesek, célzottan is megy
await clearNuxtData((key) => key.includes(':'))
// ③ és a navigáció — aldomain-modellben ez amúgy is teljes újratöltés
await navigateTo(`/t/${slug}`)
}
navigateTo(url, { external: true }) vagy egyszerűen egy sima link). Ezzel minden kliensállapot eltűnik, és nem kell emlékezned rá, mit felejtettél el invalidálni. Aldomain- és egyedi domain-modellben ez amúgy is automatikus, mert más az origin. Az „elegáns” kliensoldali tenantváltás a leggyakoribb forrása annak, hogy egy pillanatra a másik munkaterület adata villan fel.Mert a modul-scope nem része a kérés-kontextusnak. A modul egyszer értékelődik ki az izolátum élettartama alatt, és az összes kérés ugyanazokat a modul-szintű változókat látja. A kérés-hatókörű dolgokat kifejezetten kérés-hatókörű helyre kell tenni: useState az app oldalon, event.context a szerveren.
Mert az izolátumok hosszú életűek, újrahasznosulnak a kérések között, és egyszerre több kérést is kiszolgálhatnak. Node-nál is létező hiba, de ott a szokásos processz-per-instance felállás és a rövidebb élettartamok néha elfedik. A Cloudflare ezért teszi be a hivatalos Workers best practices listájába: „avoid global mutable state”.
server/utils/context.ts-ben van egy let currentTenant, amit middleware-ből állítasz be. Mi a baj, és mi helyette a helyes?
Két párhuzamos kérésnél az egyik felülírja a másik tenantját, így lekérdezések futhatnak a rossz tenanttal — ez a legsúlyosabb változata a hibának, mert az adatréteget érinti. Helyette: event.context.tenant a szerver middleware-ben, és az event-et add át végig a hívási láncon.
Az URL query-paraméterében. Így a link megosztható, a vissza-gomb működik, az SSR már az első rendereléskor a helyes listát adja, és a válasz cache-elhető marad. useState-ben tárolva mindezt elveszted.
useState-be, és mit nem?
Beleteszed: komponensek közt megosztott, az SSR-t túlélő kliensállapotot — bejelentkezett felhasználó, aktuális tenant, UI-preferencia. Nem teszed bele: szerveri adatot (az useFetch dolga), URL-be való nézetállapotot, bizalmas mezőt (a payloadba kerül), és függvényt vagy osztálypéldányt (nem szerializálható).
Teljes oldalbetöltés (külső navigáció vagy sima link). Ezzel minden kliensállapot és minden lekért adat eltűnik, tehát nem kell emlékezni rá, mit kellett volna invalidálni. A kliensoldali tenantváltásnál minden felejtésből egy pillanatnyi „a másik munkaterület adata villan fel” lesz — és ha a kulcsaid nem tenant-prefixesek, akkor nem is csak pillanatnyi.