skip to content

In Nuxt 4, how do you add Pinia through @pinia/nuxt, and what SSR work does the module do so you do not write it?

level: juniorimportance: should knowfreq 36%

answer

  1. one line in modules
  2. a pinia per Nuxt app
  3. state travels in the payload
  4. app/stores is auto-imported
  5. devtools API is a peer now

basics

~20 s

Install pinia and @pinia/nuxt and list '@pinia/nuxt' in modules. The module creates and installs a pinia per request, ships its state in Nuxt's payload, restores it on the client, and auto-imports defineStore, storeToRefs and your stores.

solid answer

~40 s

`npx nuxi@latest module add pinia` installs the packages and adds `'@pinia/nuxt'` to `modules` in `nuxt.config.ts`; with Pinia 4 `@vue/devtools-api` is a required peer too, so install it if it is missing. The module's runtime plugin creates a **fresh pinia for each Nuxt app**, which on the server means each request, installs it and makes it active. After the server render it copies `pinia.state.value` into Nuxt's payload, which Nuxt serialises safely, and on the client it assigns that payload back before stores run. It also auto-imports `defineStore`, `storeToRefs`, `acceptHMRUpdate` and `usePinia`, plus every store file directly inside `stores/` under the source directory (`app/stores` in Nuxt 4); nested folders need the `pinia.storesDirs` option.

code

bash · 2 lines
bash
npx nuxi@latest module add pinia
npm i pinia @vue/devtools-api

go deeper

for a junior

Know how to add @pinia/nuxt to modules, where stores go in Nuxt 4 (app/stores), and that defineStore and your stores are auto-imported.

for a middle

Explain what the module does per request: create and install a pinia, move its state into the payload, restore it on the client.

for a senior

Know the edges: explicit pinia outside Nuxt contexts, skipHydrate values dropped from the payload, nested stores needing storesDirs, and the Pinia 4 peer.

for a principal

Judge when Nuxt's own useState() suffices and when Pinia's stores, devtools and plugins justify the extra layer in a Nuxt codebase.

## Installing it The Pinia docs give one command: ```bash npx nuxi@latest module add pinia ``` It installs `@pinia/nuxt` and `pinia` and registers the module: ```ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], }) ``` Since Pinia 4, `@vue/devtools-api` is a **required peer dependency** of `pinia`; if the package manager did not install it, add it yourself alongside `pinia`. `@pinia/nuxt` 1.0 declares compatibility with Nuxt 3.15 and later, 4 and 5. ## What the module does at runtime The module registers a Nuxt plugin that runs for every Nuxt app instance, which on the server means every request: 1. It calls `createPinia()`, installs the pinia into the Vue app and makes it the active pinia. 2. If the Nuxt payload already contains Pinia state (on the client, after a server render), it assigns it to `pinia.state.value` straight away, before your pages and middleware call stores. 3. It provides the pinia as `$pinia` on the Nuxt app, reachable through the auto-imported `usePinia()`. 4. After the server render (the `app:rendered` hook) it copies `pinia.state.value` into the payload and clears the active pinia so the server does not keep a reference to a finished request. Everything a hand-rolled SSR setup does by hand is therefore covered: | Concern | Hand-rolled SSR | With `@pinia/nuxt` | |---|---|---| | One pinia per request | create it in your app factory | the module's plugin does it | | Serialise `pinia.state.value` | you pick and escape a serialiser | Nuxt's payload serialiser handles it | | Restore before stores run | assign it in the client entry | the plugin assigns it on start | | Values marked with `skipHydrate()` | still serialised | dropped from the payload | The Pinia docs sum it up: with Nuxt you do not need to care about serialisation or XSS attacks. ## Auto-imports and the stores folder The module adds these auto-imports: - `defineStore()` and `storeToRefs()`; - `acceptHMRUpdate()` for hot module replacement (in development the module also injects HMR handling into store files that do not already call it); - `usePinia()`, which returns the app's pinia. It also auto-imports every store exported from files directly inside `stores/`, resolved against Nuxt's source directory. In the Nuxt 4 layout that directory defaults to `app/`, so stores live in `app/stores/`. Nested folders are not scanned unless you configure them: ```ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], pinia: { storesDirs: ['./stores/**', './custom-folder/stores/**'] }, }) ``` ## Using stores in a Nuxt app In components, `const cart = useCartStore()` works as in any Vue app. Nuxt runs its plugins and route middleware inside the app's context, so a store called at the top of those also finds the right pinia. In code outside such a context, pass the pinia explicitly, for example `useCartStore(usePinia())` obtained earlier, or `nuxtApp.$pinia`. For awaiting store actions during rendering, the Pinia docs point at Nuxt's own `callOnce()`, which belongs to Nuxt's data-fetching tools. ## Upgrading an existing Nuxt app to Pinia 4 Pinia 4 contains only technically breaking changes, and `@pinia/nuxt` 1.0 has no major changes of its own. What to check: 1. **ESM only**: Pinia 4 dropped its CommonJS build; Nuxt's build is ESM already, so this mainly affects custom scripts that `require()` Pinia. 2. **The devtools peer**: add `@vue/devtools-api` to dependencies if the install does not pull it in. 3. **Error output**: development errors and warnings are now coded diagnostics such as `PINIA_R1004`, so searches for old message texts in logs or tests may need updating. 4. **Removed APIs from Pinia 3** still apply: `defineStore({ id })` is gone, and the id is the first argument. ## Why not wire Pinia into Nuxt by hand It is possible to write a Nuxt plugin that creates a pinia and moves its state through the payload, and the module is only a few files. The reasons to use the module anyway are the details it gets right: registering its plugin before the router plugin, clearing the active pinia after each server render, dropping `skipHydrate()` values from the payload, the stores auto-import and the development HMR handling. Each is small, and each is a bug when forgotten. ## Common mistakes - Also creating a pinia in a custom Nuxt plugin, which gives the app two instances. - Putting stores in nested folders and wondering why they are not auto-imported. - Confusing Nuxt's `useState()` with Pinia: it is a separate, Nuxt-owned primitive, not a store. - Forgetting `@vue/devtools-api` after upgrading to Pinia 4.

  • In a Nuxt 4 app with @pinia/nuxt, how does a server-only utility called outside any Nuxt context reach the right store?
    Pass the pinia explicitly. Obtain it where a Nuxt context exists, with `usePinia()` or `nuxtApp.$pinia`, and hand it to the utility, which calls `useCartStore(pinia)`. Relying on the active pinia outside a context risks another request's instance on a busy server.
  • Does @pinia/nuxt replace Nuxt's useState()?
    No. `useState()` is Nuxt's own SSR-friendly keyed state, owned by Nuxt. `@pinia/nuxt` only wires Pinia into Nuxt: a pinia per app, state in the payload, auto-imports. Teams use Pinia when they want stores with actions, getters, devtools and plugins.

saying these in an interview costs you the question

  • You must serialise pinia.state.value yourself even when using @pinia/nuxt
  • @pinia/nuxt creates one pinia for the whole server process
  • Stores in nested folders under stores are auto-imported by default
  • pinia alone is enough with Pinia 4; @vue/devtools-api is optional
  • Nuxt's useState() is how @pinia/nuxt stores its state