Nuxt.js Integration
This guide covers how to integrate and use Pinia in Nuxt.js applications, including complete SSR support.
Installation
Using the Nuxt Module
Recommended approach using the official @pinia/nuxt
module:
npm install pinia @pinia/nuxt
Configure Nuxt
Add the module to your nuxt.config.ts
:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@pinia/nuxt',
],
// Optional: Configure Pinia
pinia: {
autoImports: [
// Automatically import `defineStore()`
'defineStore',
// Automatically import `defineStore()` as `definePiniaStore()`
['defineStore', 'definePiniaStore'],
],
},
})
Basic Usage
Creating Stores
Create stores in the stores/
directory:
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
const login = async (credentials: LoginCredentials) => {
const { data } = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = data.user
}
const logout = async () => {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
}
return {
user: readonly(user),
isLoggedIn,
login,
logout
}
})
Using in Components
<!-- pages/profile.vue -->
<template>
<div>
<div v-if="userStore.isLoggedIn">
<h1>Welcome, {{ userStore.user.name }}!</h1>
<button @click="userStore.logout">Logout</button>
</div>
<div v-else>
<h1>Please Login</h1>
<LoginForm @login="userStore.login" />
</div>
</div>
</template>
<script setup>
const userStore = useUserStore()
// Pre-fetch data on server
if (process.server && userStore.user) {
await userStore.fetchUserProfile()
}
</script>
SSR Support
Server-side Data Fetching
Combine Nuxt's data fetching features with Pinia:
<!-- pages/products/index.vue -->
<template>
<div>
<h1>Product List</h1>
<div v-if="pending">Loading...</div>
<div v-else>
<ProductCard
v-for="product in productStore.products"
:key="product.id"
:product="product"
/>
</div>
</div>
</template>
<script setup>
const productStore = useProductStore()
// Use useLazyAsyncData for data pre-fetching
const { pending } = await useLazyAsyncData('products', async () => {
if (!productStore.products.length) {
await productStore.fetchProducts()
}
})
// Or use refresh method
const refresh = () => productStore.fetchProducts()
</script>
State Hydration
Pinia automatically handles server-to-client state hydration:
// stores/app.ts
export const useAppStore = defineStore('app', () => {
const settings = ref({
theme: 'light',
language: 'en-US'
})
const initializeApp = async () => {
// This method runs on both server and client
if (process.server) {
// Server-side initialization logic
const config = await $fetch('/api/config')
settings.value = { ...settings.value, ...config }
} else {
// Client-side initialization logic
const savedSettings = localStorage.getItem('app-settings')
if (savedSettings) {
settings.value = { ...settings.value, ...JSON.parse(savedSettings) }
}
}
}
return {
settings,
initializeApp
}
})
Plugin Integration
Creating Pinia Plugins
// plugins/pinia.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(({ store }) => {
// Client-only plugin logic
if (process.client) {
// Add persistence
const savedState = localStorage.getItem(`pinia-${store.$id}`)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// Watch changes and save
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
}
})
})
Server-side Plugins
// plugins/pinia.server.ts
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(({ store }) => {
// Server-specific plugin logic
if (process.server) {
// Add server-side logging
store.$onAction(({ name, args }) => {
console.log(`[Server] Action ${name} called with:`, args)
})
}
})
})
Middleware Integration
Route Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
return navigateTo('/login')
}
})
Global Middleware
// middleware/app.global.ts
export default defineNuxtRouteMiddleware(async (to) => {
const appStore = useAppStore()
// Ensure app is initialized
if (!appStore.isInitialized) {
await appStore.initialize()
}
// Set page title based on route
if (to.meta.title) {
appStore.setPageTitle(to.meta.title)
}
})
API Integration
Using $fetch
// stores/api.ts
export const useApiStore = defineStore('api', () => {
const loading = ref(false)
const error = ref(null)
const apiCall = async <T>(url: string, options: any = {}): Promise<T> => {
loading.value = true
error.value = null
try {
const data = await $fetch<T>(url, {
...options,
onRequest({ request, options }) {
// Add auth headers
const userStore = useUserStore()
if (userStore.token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${userStore.token}`
}
}
},
onResponseError({ response }) {
// Handle auth errors
if (response.status === 401) {
const userStore = useUserStore()
userStore.logout()
navigateTo('/login')
}
}
})
return data
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}
return {
loading: readonly(loading),
error: readonly(error),
apiCall
}
})
Reactive API Calls
// composables/useApi.ts
export const useApi = <T>(url: string, options: any = {}) => {
const apiStore = useApiStore()
return useLazyAsyncData<T>(url, () =>
apiStore.apiCall<T>(url, options)
)
}
// Usage in components
const { data: products, pending, error, refresh } = await useApi('/api/products')
Type Safety
Store Type Definitions
// types/stores.ts
export interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
export interface UserState {
user: User | null
token: string | null
preferences: UserPreferences
}
export interface UserPreferences {
theme: 'light' | 'dark'
language: string
notifications: boolean
}
Typed Stores
// stores/user.ts
import type { User, UserState, UserPreferences } from '~/types/stores'
export const useUserStore = defineStore('user', (): UserState & {
// Actions
login(credentials: LoginCredentials): Promise<void>
logout(): Promise<void>
updatePreferences(prefs: Partial<UserPreferences>): void
// Getters
isLoggedIn: ComputedRef<boolean>
isAdmin: ComputedRef<boolean>
} => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const preferences = ref<UserPreferences>({
theme: 'light',
language: 'en-US',
notifications: true
})
const isLoggedIn = computed(() => !!user.value && !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
const login = async (credentials: LoginCredentials) => {
const response = await $fetch<{ user: User; token: string }>('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = response.user
token.value = response.token
}
const logout = async () => {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
token.value = null
}
const updatePreferences = (prefs: Partial<UserPreferences>) => {
preferences.value = { ...preferences.value, ...prefs }
}
return {
user: readonly(user),
token: readonly(token),
preferences,
isLoggedIn,
isAdmin,
login,
logout,
updatePreferences
}
})
Testing
Unit Testing
// tests/stores/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '~/stores/user'
// Mock $fetch
vi.mock('#app', () => ({
$fetch: vi.fn()
}))
describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('should handle login correctly', async () => {
const store = useUserStore()
const mockUser = { id: '1', name: 'John', email: 'john@example.com', role: 'user' }
const mockToken = 'mock-token'
// Mock API response
vi.mocked($fetch).mockResolvedValueOnce({
user: mockUser,
token: mockToken
})
await store.login({ email: 'john@example.com', password: 'password' })
expect(store.user).toEqual(mockUser)
expect(store.token).toBe(mockToken)
expect(store.isLoggedIn).toBe(true)
})
it('should handle logout correctly', async () => {
const store = useUserStore()
// Set initial state
store.$patch({
user: { id: '1', name: 'John', email: 'john@example.com', role: 'user' },
token: 'mock-token'
})
vi.mocked($fetch).mockResolvedValueOnce({})
await store.logout()
expect(store.user).toBeNull()
expect(store.token).toBeNull()
expect(store.isLoggedIn).toBe(false)
})
})
Integration Testing
// tests/integration/auth.test.ts
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import LoginPage from '~/pages/login.vue'
describe('Authentication Integration Tests', () => {
it('should render login page correctly', async () => {
const component = await mountSuspended(LoginPage)
expect(component.text()).toContain('Login')
})
it('should handle login flow', async () => {
const component = await mountSuspended(LoginPage)
// Simulate user input
await component.find('input[type="email"]').setValue('test@example.com')
await component.find('input[type="password"]').setValue('password')
// Submit form
await component.find('form').trigger('submit')
// Verify state changes
const userStore = useUserStore()
expect(userStore.isLoggedIn).toBe(true)
})
})
Performance Optimization
Lazy Loading Stores
// composables/useLazyStore.ts
export const useLazyStore = <T>(storeFactory: () => T): Promise<T> => {
return new Promise((resolve) => {
if (process.client) {
// Client-side lazy loading
nextTick(() => {
resolve(storeFactory())
})
} else {
// Server-side immediate loading
resolve(storeFactory())
}
})
}
// Usage example
const store = await useLazyStore(() => useHeavyStore())
Code Splitting
// stores/index.ts
export const useUserStore = () => import('./user').then(m => m.useUserStore)
export const useProductStore = () => import('./product').then(m => m.useProductStore)
export const useCartStore = () => import('./cart').then(m => m.useCartStore)
// Usage in components
const loadUserStore = async () => {
const { useUserStore } = await import('~/stores/user')
return useUserStore()
}
Deployment Considerations
Environment Variables
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-side environment variables
apiSecret: process.env.API_SECRET,
public: {
// Client-side environment variables
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
}
})
// Usage in stores
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
Production Optimization
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
// Pre-render routes
prerender: {
routes: ['/sitemap.xml']
}
},
// Build optimization
build: {
transpile: ['pinia']
},
// Experimental features
experimental: {
payloadExtraction: false // If using large amounts of store data
}
})
Best Practices
Store Organization
stores/
├── index.ts # Store exports
├── user.ts # User-related
├── auth.ts # Authentication-related
├── products/ # Product module
│ ├── index.ts
│ ├── list.ts
│ └── detail.ts
└── utils/ # Store utilities
├── api.ts
└── cache.ts
Error Handling
// stores/error.ts
export const useErrorStore = defineStore('error', () => {
const errors = ref<Array<AppError>>([])
const addError = (error: AppError) => {
errors.value.push(error)
// Auto-clear errors
setTimeout(() => {
removeError(error.id)
}, 5000)
}
const removeError = (id: string) => {
const index = errors.value.findIndex(e => e.id === id)
if (index > -1) {
errors.value.splice(index, 1)
}
}
const clearErrors = () => {
errors.value = []
}
return {
errors: readonly(errors),
addError,
removeError,
clearErrors
}
})
// Global error handling
export default defineNuxtPlugin(() => {
const errorStore = useErrorStore()
// Catch unhandled errors
if (process.client) {
window.addEventListener('unhandledrejection', (event) => {
errorStore.addError({
id: Date.now().toString(),
message: event.reason.message || 'Unknown error',
type: 'error'
})
})
}
})
Development Tools
// plugins/devtools.client.ts
export default defineNuxtPlugin(() => {
if (process.dev) {
// Development environment debugging tools
window.__PINIA_STORES__ = {}
const pinia = usePinia()
pinia.use(({ store }) => {
window.__PINIA_STORES__[store.$id] = store
})
}
})
Common Questions
Q: How to use Pinia in Nuxt 3?
A: Use the @pinia/nuxt
module, which provides complete Nuxt 3 support including auto-imports and SSR.
Q: What if state is inconsistent between server and client?
A: Ensure you use the same data fetching logic on both server and client, and properly handle async operations.
Q: How to persist store state in Nuxt?
A: Use plugins to save state to localStorage on the client, and restore from cookies or database on the server.
Q: How to handle authentication state?
A: Use middleware to check auth state, restore user info from cookies on server, and listen for auth changes on client.