Learn Pinia - SSR & Nuxt Integration
Episode 11 of 23

Learn Pinia - SSR & Nuxt Integration

Server-Side Rendering poses a unique challenge for state: a Pinia instance must be created per request to avoid a shared singleton state. This episode covers Pinia SSR fundamentals, the @pinia/nuxt module for auto-setup, and how to handle hydration state and use stores in definePageMeta.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

In a typical SPA, Pinia is created once and used forever. In a Server-Side Rendering app, every request that hits the server must get clean state — otherwise user A's data could leak to user B. This is SSR's main challenge and the reason Pinia was designed with SSR awareness from the start.

Episode 11 covers three things: Pinia SSR fundamentals, Nuxt integration through @pinia/nuxt, and handling hydration state. If you're building with Nuxt, most of this episode will read like a production guide.

SSR Fundamentals: A Per-Request Instance

On the server, never create a single global Pinia. Every request must get a new instance:

JSPer-request Pinia instance on the server
export function createApp() {
  const app = createApp(App)
  const pinia = createPinia()
  app.use(pinia)
  return { app, pinia }
}
 
// server.js
server.get('/app', (req, res) => {
  const { app, pinia } = createApp()
  // render the app here, then send the state to the client
})

createApp() is called for every request, so createPinia() produces a new instance. Without this pattern, two users arriving at the same time would share the same state — a bug that leaks data between requests.

Transferring State to the Client

After rendering on the server, Pinia's state must be sent to the client for rehydration. A common pattern: serialize pinia.state.value into the HTML, then use it as initialState on the client:

JSSerialize and hydrate state
// Server: send the state
const state = JSON.stringify(pinia.state.value)
 
// Client: use it as the initial state
const pinia = createPinia()
pinia.state.value = JSON.parse(window.__PINIA_STATE__)

pinia.state.value holds the state of all stores in a single object keyed by store id. This synchronization between server and client is what prevents hydration mismatch.

Nuxt Integration with @pinia/nuxt

Nuxt automates all the steps above through the @pinia/nuxt module. Install it and register it in the config:

Install the Nuxt module
npm i pinia @pinia/nuxt
nuxt.config.ts
modules:
  - '@pinia/nuxt'

Once the module is active, defineStore and useStore() work directly inside setup components, pages, and plugins without boilerplate. The module handles creating the per-request instance, injecting it into the app, and hydration automatically.

Using Stores in Nuxt

In Nuxt, stores are used as usual inside setup:

JSStore in a Nuxt page
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
 
const counter = useCounterStore()
</script>

useCounterStore() in Nuxt automatically uses the active Pinia instance managed by the module. To call a store from outside setup — for example in definePageMeta or a plugin — use callWithNuxt so the store runs in the correct Nuxt context:

JSCalling a store outside setup
import { callWithNuxt, useNuxtApp } from '#app'
import { useUserStore } from '@/stores/user'
 
const nuxt = useNuxtApp()
 
callWithNuxt(nuxt, () => {
  const userStore = useUserStore()
  userStore.fetchProfile()
})

callWithNuxt(nuxt, fn) runs the callback inside the currently active Nuxt app context, so useUserStore() finds the correct Pinia instance for that request. In plugins, Nuxt already provides nuxtApp as an argument — just wrap the store code with callWithNuxt inside it.

An alternative is reading the Pinia instance directly via useNuxtApp().$pinia. This route is suitable for accessing custom Pinia plugins registered with pinia.use() on the Nuxt side, because the @pinia/nuxt module exposes the same instance to the entire app.

Warning

Don't call stores at the module level (top-level imports) in Nuxt. Stores must be called in a setup context or in a function run by Nuxt, because a new Pinia instance is created per request.

Closing

Episode 11 opens Pinia up to the world of SSR. You now understand the reasoning behind per-request instances, the state serialization and hydration pattern, easy integration with @pinia/nuxt, and how to use stores inside Nuxt without boilerplate.

Key takeaways:

  • Every request on the server must get a new Pinia instance.
  • Shared singleton state on the server risks leaking between users.
  • State is sent to the client through serialization of pinia.state.value.
  • @pinia/nuxt handles setup, hydration, and per-request instances.
  • Stores are used in Nuxt setup as usual.
  • Call stores outside setup with callWithNuxt.

In the next episode, episode 12, we'll discuss composing stores — using stores inside stores, splitting a domain into small stores like auth, cart, and ui, and coordinating between stores. This is the pattern that takes Pinia from small apps to large ones.

Learn Pinia - SSR & Nuxt Integration | Learning Pinia