Nuxt State Management
Share reactive state across components with SSR-safe useState and Pinia store hydration
When to Use
- You need state that persists across navigation without re-fetching (user session, cart, theme)
- You are seeing hydration mismatch errors caused by state divergence between server and client
- You want to use Pinia stores in a Nuxt SSR context with proper server-to-client serialization
- You need to share state between unrelated components without prop-drilling
Instructions
useState — built-in SSR-safe state:
- Use
useStateinstead ofreffor any state that must be consistent between server and client renders.useStateis keyed: the same key always returns the same reactive reference:
// composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0);
<!-- pages/index.vue -->
<script setup>
const counter = useCounter();
</script>
<template>
<button @click="counter++">{{ counter }}</button>
</template>
- The second argument to
useStateis an initializer factory — it runs only once on the server and is never called again on the client (state is transferred via payload):
const user = useState<User | null>('current-user', () => null);
- Always provide a unique, descriptive key to avoid collisions across different composables:
// Prefer namespaced keys for large apps
const cartItems = useState<CartItem[]>('cart:items', () => []);
const cartTotal = useState<number>('cart:total', () => 0);
- Reset state on the server-per-request boundary using
clearNuxtState— useful for user-specific data:
// plugins/reset-state.ts
export default defineNuxtPlugin(() => {
addRouteMiddleware(() => {
clearNuxtState(['cart:items', 'cart:total']);
});
});
Pinia — structured stores:
- Install
@pinia/nuxtand add it tomodulesinnuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
});
- Define stores using
defineStore— they are auto-imported when placed instores/(withimports.dirsconfigured):
// stores/useAuthStore.ts
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null);
const isAuthenticated = computed(() => !!user.value);
async function login(credentials: Credentials) {
user.value = await $fetch('/api/auth/login', { method: 'POST', body: credentials });
}
function logout() {
user.value = null;
}
return { user, isAuthenticated, login, logout };
});
- Hydrate Pinia stores server-side using
useAsyncDatawithin the store or in the page:
// Hydrate in page
const authStore = useAuthStore();
await useAsyncData('auth', () => authStore.fetchCurrentUser());
- Use
pinia.state.valuein server plugins to initialize store state from server-side sources:
// plugins/init-state.server.ts
export default defineNuxtPlugin(async (nuxtApp) => {
const authStore = useAuthStore(nuxtApp.$pinia);
const sessionUser = await getSessionUser(useRequestEvent());
authStore.user = sessionUser;
});
Details
Why ref causes hydration mismatches:
A plain ref in a composable creates a new reactive instance per component call. During SSR, the server renders with its own instance; on the client, a fresh ref initializes to the default value — causing a mismatch. useState solves this by storing state in Nuxt's SSR payload and rehydrating from it on the client.
useState vs. Pinia:
| Concern | useState | Pinia |
|---|---|---|
| Simple scalar/object state | Best fit | Overkill |
| Complex logic, actions, getters | Awkward | Best fit |
| DevTools time-travel | No | Yes |
| Plugin ecosystem | None | Rich |
| SSR safety | Built-in | Requires setup |
Serialization requirements:
State transferred via the SSR payload must be JSON-serializable. Do not store class instances, functions, or circular references in useState or Pinia stores. Use plain objects and primitives.
Pinia store persistence (client-only):
Use pinia-plugin-persistedstate for localStorage sync. Mark it client-only to avoid SSR issues:
// plugins/pinia-persist.client.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(piniaPluginPersistedstate);
});
Avoiding state pollution between requests:
In SSR, all requests share the same module scope. Never use module-level ref or reactive for per-user state — it leaks between requests. Always use useState (keyed per request) or Pinia (reset via $reset() in server middleware).
Source
https://nuxt.com/docs/getting-started/state-management
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.