"Where does this state live" is the question behind most frontend architecture interviews. In a Nuxt app the honest answer is "it depends on scope, lifetime and who else needs it", and a senior candidate can name the eight places state can live and the trade-off of each in one breath. This section is that breath, expanded.
ref in setup): the default; local, dies with the component. Lift it only when a sibling or a distant component needs it.useState): SSR-safe, keyed, serialised into the payload, no behaviour attached. Right for small cross-component values; wrong for anything with actions or a lifecycle beyond the page.useAsyncData, or a query library such as TanStack Query or Pinia Colada): data owned by the server that the client caches, deduplicates, invalidates and refetches. Do not copy it into a store; wrap it in a cache with a key.route.query, params): anything the user should be able to share, bookmark or navigate back to: filters, pagination, selected tab, sort order.useCookie): preferences the server render must know (locale, colour mode, consent) and the session itself.localStorage, IndexedDB, Cache API): per-device persistence that the server cannot see; drafts, offline data, large caches. Never anything secret, never anything the first render depends on without a hydration strategy.| Page | Question it answers |
|---|---|
| Pinia | "When do you use a store, and how does Pinia work in SSR?" |
| Server state and caching | "How do you cache, invalidate and update data from the API on the client?" |
| Forms and validation | "How do you build forms with one schema for client and server?" |
| Realtime and sync | "WebSockets, SSE or polling, and how do you keep clients in sync?" |
| URL and persistence | "What goes in the URL, a cookie or localStorage, and how do you persist without breaking hydration?" |
The decision as a table, which is how I would draw it on a whiteboard:
| Need | Place | Because |
|---|---|---|
| Modal open, input draft | ref in the component | nobody else cares |
| Sidebar collapsed, across components on one page | useState('ui:sidebar') | shared, tiny, SSR-safe |
| Current user, permissions | Pinia store, hydrated from the payload | behaviour (login, logout), read everywhere |
| Product list from the API | useFetch / query cache keyed by params | server-owned; cache and invalidate, do not copy |
| Active filters, page number | URL query | shareable, back button works, server can render it |
| Locale, theme, cookie consent | useCookie | the server render needs it |
| Unsent comment draft | localStorage, restored after hydration | per device, survives reload, nothing depends on it at first render |
| Orders, inventory | database behind server/api | durable, shared, authoritative |
// URL state with typed parsing: shareable, back-button friendly, rendered by the server.
import { z } from 'zod'
const schema = z.object({
q: z.string().default(''),
page: z.coerce.number().int().min(1).default(1),
sort: z.enum(['price', 'name']).default('name'),
})
export function useProductFilters() {
const route = useRoute()
const router = useRouter()
const filters = computed(() => schema.parse(route.query))
function update(patch: Partial<z.infer<typeof schema>>) {
router.replace({ query: { ...route.query, ...patch, page: patch.page ?? 1 } })
}
return { filters, update }
}
A products array in a Pinia store filled by an action is a second source of truth with no invalidation policy. It goes stale, it leaks between users on the server if the action runs at module level, and it duplicates what useAsyncData or a query cache already does with keys and refetching. Store client-owned state; cache server-owned data.
useProductFilters and confirm that the server render honours ?page=2&sort=price and that the browser back button steps through filter changes."By scope and lifetime. Local UI state stays a ref in the component. State that several components on a page share is useState, which is SSR-safe and serialised into the payload. State with behaviour that the whole application reads, the user, a cart, is a Pinia store, one instance per request on the server and hydrated on the client. Data that the server owns is not state I copy; it lives in a keyed cache, useAsyncData or a query library, with an invalidation policy. Anything shareable or bookmarkable goes in the URL, anything the server render needs to know goes in a cookie, and browser storage is for per-device persistence that the first render does not depend on."
Nothing yet. Notes filed here show up on every page of this section.
API — commands, configuration, advanced
The Nuxt CLI and every command with its notable flags, the top-level keys of nuxt.config, the three hook families, the import.meta flags, and the error-code catalogue.
Pinia
Setup stores versus option stores, how Pinia works in Nuxt SSR, storeToRefs and the reactivity traps, plugins and persistence, testing with createTestingPinia, Pinia 4, and when a store is the wrong tool.