--- name: vue-nuxt description: "Use when building or reviewing a Vue 3 + Nuxt 4 app — ` ``` `watch` vs `watchEffect`: use `watch` when you need the previous value or an explicit dependency; use `watchEffect` for "run now and re-run when anything I touched changes". Register teardown with `onWatcherCleanup()` (Vue 3.5) or the `onCleanup` arg to cancel stale async work — why: a watcher that fires faster than its async settles will otherwise apply an out-of-order result. Reactive props destructure is **stable in Vue 3.5** — the compiler rewrites `count` to `props.count`, so the binding stays reactive and you get clean default syntax: ```vue ``` Two-way binding uses `defineModel()` (stable since 3.4), replacing the manual `props`+`emit('update:x')` pair. Template DOM refs use `useTemplateRef('name')` (3.5), not a manually-named `ref`. ```vue ``` ### Bad → Good: do not destructure a `reactive()` ```vue ``` Deep reactivity, effect scope, advanced `provide/inject`, and render-function/JSX notes live in [`references/reactivity.md`](references/reactivity.md). ## Components & composables Type `defineProps`/`defineEmits` with generics, not the runtime object form — you get compile-time checking for free: ```vue ``` Extract reusable logic into `composables/useX.ts` returning refs — **a composable, never a mixin** (why: mixins merge invisibly and collide on names; composables are explicit and tree-shakeable). No side effects at module scope (that runs once per server process and leaks across requests — see state below). Use typed `provide`/`inject` with an `InjectionKey` for dependency injection down a tree instead of prop-drilling. ## The data-fetching boundary (core) This is where most Nuxt bugs live. Pick deliberately: | API | Use it for | SSR behavior | |---|---|---| | `useFetch(url, opts)` | the common case — fetch a URL in a page/component | fetches **once on server**, transfers payload to client, no refetch on hydration | | `useAsyncData(key, fn)` | wrap custom logic / multiple `$fetch` calls / a non-URL source | same once-then-transfer; you control the fn | | `$fetch(url)` | inside event handlers, server routes, or after mount | a plain request; **NOT** for top-level `setup` data | ### Bad → Good: bare `$fetch` in setup double-fetches on SSR ```vue ``` Key options: `key` (shared/deduped result — same key returns the same `data`/`error`/`status` ref, auto-cleaned on last unmount), `lazy: true` (don't block navigation), `server: false` (client-only fetch), `transform` (reshape before storing), `pick` (keep only listed fields — shrinks payload), `watch`/reactive keys (a `ref`/`computed`/getter key refetches when it changes). Type the result with `useFetch()` / `useAsyncData()`. In **Nuxt 4 the returned `data` is a `shallowRef`** — replace the whole value, don't deep-mutate, to trigger updates. Nuxt 4.2 adds `AbortController` signal support for request cancellation. Re-run with the returned `refresh()`, or invalidate broadly with `refreshNuxtData(key)`. Full option matrix, custom `$api` factory, optimistic UI, and error/pending patterns are in [`references/data-and-state.md`](references/data-and-state.md). ## SSR-safe state On the server one Node process serves many requests. A module-level `ref` is created **once** and shared by every visitor — a textbook cross-request data leak. | Approach | Per-request? | When | |---|---|---| | module-level `ref`/`reactive` | **NO — leaks across requests** | never for request data; fine only for true constants | | `useState(key, init)` | yes — serialized after SSR, restored on hydration, shared by key | lightweight shared value | | Pinia store (`@pinia/nuxt`) | yes — hydrated from Nuxt payload | structured state, actions, multiple consumers | ### Bad → Good: module `ref` → `useState` ```ts // Bad — module scope: one instance for the whole server, shared between users. import { ref } from 'vue' export const user = ref(null) // Good — per-request, hydration-safe, shared by key. export const useUser = () => useState('user', () => null) ``` Pinia 3 (dropped Vue 2) with `@pinia/nuxt` auto-imports stores from `app/stores/`. Use the **setup-store** form; SSR state hydrates from the payload automatically: ```ts // app/stores/cart.ts export const useCartStore = defineStore('cart', () => { const items = ref([]) const count = computed(() => items.value.length) function add(i: Item) { items.value.push(i) } return { items, count, add } }) ``` ## Hydration mismatches A mismatch means the server-rendered HTML differs from the client's first render. Common causes: `Date.now()`/`new Date()`/`Math.random()` in render, reading `localStorage`/`window`/`document` in `setup`, locale/timezone differences, invalid HTML nesting (`

` wrapping a `

`), and non-deterministic iteration order. Fix kit: wrap genuinely client-only UI in ``; branch with `import.meta.client` / `import.meta.server`; do browser work in `onMounted` (never in `setup` body); and pin a server-generated value with `useState` so the client reuses the exact same value instead of recomputing it. ```vue ``` ## Nitro server routes Files in `server/api/*` and `server/routes/*` run on Nitro (Nuxt's server engine). Name by method with `.get.ts`/`.post.ts`. Validate input; throw `createError` for HTTP errors. ```ts // server/api/products/[id].get.ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') const { fields } = getQuery(event) if (!id) throw createError({ statusCode: 400, statusMessage: 'id required' }) const config = useRuntimeConfig() // private keys server-only const data = await fetchFromDb(id, config.dbUrl) if (!data) throw createError({ statusCode: 404, statusMessage: 'Not found' }) return data }) ``` `useRuntimeConfig()` exposes top-level keys **only on the server**; only `config.public.*` reaches the browser bundle. Rule: a secret in `runtimeConfig.public` (or any `NUXT_PUBLIC_*` env) ships to every client — keep API keys, DB URLs, and tokens at the top level, never under `public`. Type calls to your own API with `$fetch('/api/...')`. Handlers, route-rule recipes, middleware, and `defineCachedEventHandler` caching live in [`references/nitro-and-rendering.md`](references/nitro-and-rendering.md). ## Rendering strategy Set per-route rendering in `nuxt.config.ts` with `routeRules`; the right mix is usually hybrid, not all-SSR. ```ts export default defineNuxtConfig({ routeRules: { '/': { prerender: true }, // SSG at build '/blog/**': { swr: 3600 }, // stale-while-revalidate cache 1h '/products/**': { isr: true }, // incremental static regeneration '/admin/**': { ssr: false }, // client-only SPA island '/old': { redirect: '/new' }, '/api/**': { headers: { 'cache-control': 's-maxage=60' } }, }, }) ``` `nuxt build` produces an SSR server; `nuxt generate` prerenders a fully static site. The **Nitro preset** chooses the deploy target (node-server, vercel, netlify, cloudflare-pages, …) — pick the preset here, then hand platform specifics to [`deployment`](../deployment/SKILL.md), [`vercel`](../vercel/SKILL.md), [`netlify`](../netlify/SKILL.md), or [`cloudflare`](../cloudflare/SKILL.md). ## Performance - `shallowRef`/`shallowReactive` for large payloads/lists — skip deep proxy cost; replace the whole value to update. Why: deep reactivity on a 10k-row array is pure overhead. - `v-memo` to freeze a subtree on stable deps; `v-once` for render-once static content. - `defineAsyncComponent` and Nuxt's auto `Lazy` prefix to code-split below the fold. - `` / `@nuxt/image` for responsive, optimized images (a top LCP lever). - Shrink the SSR payload with `pick`/`transform` on `useFetch`/`useAsyncData`. - Vue 3.6 **Vapor Mode** (compile-time, no-VDOM, opt-in per component via `