State Persistence
State persistence allows your Pinia stores to maintain their state across browser sessions, page reloads, and application restarts. This guide covers various persistence strategies and implementations.
Overview
Pinia supports multiple persistence mechanisms:
- localStorage - Persistent across browser sessions
- sessionStorage - Persistent within a single session
- IndexedDB - For large amounts of data
- Cookies - For server-side access
- Custom storage - Your own persistence logic
Using pinia-plugin-persistedstate
The recommended approach is using the official persistence plugin.
Installation
bash
npm install pinia-plugin-persistedstate
Basic Setup
ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
Store Configuration
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
email: '',
preferences: {
theme: 'light',
language: 'en'
}
}),
actions: {
updateProfile(data: Partial<User>) {
Object.assign(this, data)
}
},
// Enable persistence
persist: true
})
Persistence Configuration
Storage Options
ts
// stores/settings.ts
export const useSettingsStore = defineStore('settings', {
state: () => ({
theme: 'light',
language: 'en',
notifications: true
}),
persist: {
// Use sessionStorage instead of localStorage
storage: sessionStorage,
// Custom key (default: store.$id)
key: 'app-settings',
// Only persist specific paths
paths: ['theme', 'language']
}
})
Multiple Storage Configurations
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
profile: {
name: '',
email: ''
},
preferences: {
theme: 'light',
language: 'en'
},
session: {
token: '',
lastActivity: null
}
}),
persist: [
{
// Persist profile in localStorage
key: 'user-profile',
storage: localStorage,
paths: ['profile']
},
{
// Persist session in sessionStorage
key: 'user-session',
storage: sessionStorage,
paths: ['session']
}
]
})
Custom Serialization
JSON with Date Support
ts
// utils/serialization.ts
export const dateAwareSerializer = {
serialize: (value: any) => {
return JSON.stringify(value, (key, val) => {
if (val instanceof Date) {
return { __type: 'Date', value: val.toISOString() }
}
return val
})
},
deserialize: (value: string) => {
return JSON.parse(value, (key, val) => {
if (val && val.__type === 'Date') {
return new Date(val.value)
}
return val
})
}
}
// stores/activity.ts
export const useActivityStore = defineStore('activity', {
state: () => ({
lastLogin: new Date(),
activities: []
}),
persist: {
serializer: dateAwareSerializer
}
})
Encryption Support
ts
// utils/encryption.ts
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'your-secret-key'
export const encryptedSerializer = {
serialize: (value: any) => {
const jsonString = JSON.stringify(value)
return CryptoJS.AES.encrypt(jsonString, SECRET_KEY).toString()
},
deserialize: (value: string) => {
const bytes = CryptoJS.AES.decrypt(value, SECRET_KEY)
const jsonString = bytes.toString(CryptoJS.enc.Utf8)
return JSON.parse(jsonString)
}
}
// stores/sensitive.ts
export const useSensitiveStore = defineStore('sensitive', {
state: () => ({
apiKeys: {},
personalData: {}
}),
persist: {
serializer: encryptedSerializer
}
})
Advanced Persistence Patterns
Conditional Persistence
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
rememberMe: false,
sessionData: {}
}),
actions: {
login(credentials: LoginCredentials, remember: boolean) {
this.rememberMe = remember
// Login logic...
}
},
persist: {
// Only persist if user chose "Remember Me"
enabled: (store) => store.rememberMe,
paths: ['user']
}
})
Version Migration
ts
// stores/app.ts
const CURRENT_VERSION = '2.0.0'
export const useAppStore = defineStore('app', {
state: () => ({
version: CURRENT_VERSION,
settings: {},
data: []
}),
persist: {
beforeRestore: (context) => {
const stored = context.store.$state
// Migrate from v1.x to v2.x
if (!stored.version || stored.version.startsWith('1.')) {
// Perform migration
stored.settings = migrateV1Settings(stored.settings)
stored.version = CURRENT_VERSION
}
}
}
})
function migrateV1Settings(oldSettings: any) {
// Migration logic
return {
...oldSettings,
newProperty: 'defaultValue'
}
}
Selective Hydration
ts
// stores/cache.ts
export const useCacheStore = defineStore('cache', {
state: () => ({
userCache: new Map(),
apiCache: new Map(),
tempData: {}
}),
persist: {
// Custom hydration logic
afterRestore: (context) => {
const store = context.store
// Convert plain objects back to Maps
if (store.userCache && !(store.userCache instanceof Map)) {
store.userCache = new Map(Object.entries(store.userCache))
}
if (store.apiCache && !(store.apiCache instanceof Map)) {
store.apiCache = new Map(Object.entries(store.apiCache))
}
// Clear temporary data
store.tempData = {}
},
// Custom serialization for Maps
serializer: {
serialize: (value) => {
const serializable = { ...value }
if (value.userCache instanceof Map) {
serializable.userCache = Object.fromEntries(value.userCache)
}
if (value.apiCache instanceof Map) {
serializable.apiCache = Object.fromEntries(value.apiCache)
}
return JSON.stringify(serializable)
},
deserialize: JSON.parse
}
}
})
IndexedDB Integration
Using Dexie.js
ts
// utils/indexeddb.ts
import Dexie from 'dexie'
class AppDatabase extends Dexie {
stores!: Dexie.Table<any, string>
constructor() {
super('AppDatabase')
this.version(1).stores({
stores: 'id, data, timestamp'
})
}
}
const db = new AppDatabase()
export const indexedDBStorage = {
getItem: async (key: string) => {
const item = await db.stores.get(key)
return item?.data
},
setItem: async (key: string, value: any) => {
await db.stores.put({
id: key,
data: value,
timestamp: Date.now()
})
},
removeItem: async (key: string) => {
await db.stores.delete(key)
}
}
// stores/large-data.ts
export const useLargeDataStore = defineStore('largeData', {
state: () => ({
documents: [],
images: [],
cache: new Map()
}),
persist: {
storage: indexedDBStorage
}
})
Cookie-based Persistence
Server-Side Compatible
ts
// utils/cookie-storage.ts
import Cookies from 'js-cookie'
export const cookieStorage = {
getItem: (key: string) => {
return Cookies.get(key)
},
setItem: (key: string, value: string) => {
Cookies.set(key, value, {
expires: 30, // 30 days
secure: true,
sameSite: 'strict'
})
},
removeItem: (key: string) => {
Cookies.remove(key)
}
}
// stores/user-preferences.ts
export const useUserPreferencesStore = defineStore('userPreferences', {
state: () => ({
theme: 'light',
language: 'en',
timezone: 'UTC'
}),
persist: {
storage: cookieStorage,
// Limit data size for cookies
paths: ['theme', 'language']
}
})
Performance Optimization
Debounced Persistence
ts
// utils/debounced-storage.ts
import { debounce } from 'lodash-es'
function createDebouncedStorage(storage: Storage, delay = 1000) {
const debouncedSetItem = debounce(
(key: string, value: string) => {
storage.setItem(key, value)
},
delay
)
return {
getItem: (key: string) => storage.getItem(key),
setItem: debouncedSetItem,
removeItem: (key: string) => storage.removeItem(key)
}
}
// stores/frequent-updates.ts
export const useFrequentUpdatesStore = defineStore('frequentUpdates', {
state: () => ({
counter: 0,
lastUpdate: Date.now()
}),
actions: {
increment() {
this.counter++
this.lastUpdate = Date.now()
}
},
persist: {
storage: createDebouncedStorage(localStorage, 2000)
}
})
Compression
ts
// utils/compressed-storage.ts
import LZString from 'lz-string'
export const compressedStorage = {
getItem: (key: string) => {
const compressed = localStorage.getItem(key)
if (!compressed) return null
try {
return LZString.decompress(compressed)
} catch {
return null
}
},
setItem: (key: string, value: string) => {
const compressed = LZString.compress(value)
localStorage.setItem(key, compressed)
},
removeItem: (key: string) => {
localStorage.removeItem(key)
}
}
// stores/large-state.ts
export const useLargeStateStore = defineStore('largeState', {
state: () => ({
largeArray: [],
complexObject: {}
}),
persist: {
storage: compressedStorage
}
})
Testing Persistence
Mock Storage
ts
// tests/utils/mock-storage.ts
export class MockStorage implements Storage {
private data: Record<string, string> = {}
get length() {
return Object.keys(this.data).length
}
getItem(key: string): string | null {
return this.data[key] || null
}
setItem(key: string, value: string): void {
this.data[key] = value
}
removeItem(key: string): void {
delete this.data[key]
}
clear(): void {
this.data = {}
}
key(index: number): string | null {
const keys = Object.keys(this.data)
return keys[index] || null
}
}
Test Example
ts
// tests/stores/user.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
import { MockStorage } from '../utils/mock-storage'
describe('User Store Persistence', () => {
let mockStorage: MockStorage
beforeEach(() => {
mockStorage = new MockStorage()
// Mock localStorage
Object.defineProperty(window, 'localStorage', {
value: mockStorage,
writable: true
})
setActivePinia(createPinia())
})
it('should persist user data', () => {
const store = useUserStore()
store.updateProfile({
name: 'John Doe',
email: 'john@example.com'
})
// Check if data was persisted
const persistedData = mockStorage.getItem('user')
expect(persistedData).toBeTruthy()
const parsed = JSON.parse(persistedData!)
expect(parsed.name).toBe('John Doe')
expect(parsed.email).toBe('john@example.com')
})
it('should restore persisted data', () => {
// Pre-populate storage
mockStorage.setItem('user', JSON.stringify({
name: 'Jane Doe',
email: 'jane@example.com'
}))
const store = useUserStore()
expect(store.name).toBe('Jane Doe')
expect(store.email).toBe('jane@example.com')
})
})
Best Practices
1. Choose Appropriate Storage
ts
// Guidelines for storage selection
const storageGuidelines = {
localStorage: {
use: 'User preferences, settings, long-term data',
avoid: 'Sensitive data, large datasets (>5MB)',
maxSize: '5-10MB'
},
sessionStorage: {
use: 'Temporary data, session-specific state',
avoid: 'Data needed across sessions',
maxSize: '5-10MB'
},
indexedDB: {
use: 'Large datasets, complex objects, offline data',
avoid: 'Simple key-value pairs',
maxSize: 'Hundreds of MB to GB'
},
cookies: {
use: 'Server-side accessible data, authentication tokens',
avoid: 'Large data, client-only data',
maxSize: '4KB per cookie'
}
}
2. Handle Storage Errors
ts
// stores/resilient.ts
export const useResilientStore = defineStore('resilient', {
state: () => ({
data: [],
lastSaved: null
}),
persist: {
beforeRestore: (context) => {
try {
// Validate stored data
const stored = context.store.$state
if (!Array.isArray(stored.data)) {
throw new Error('Invalid data format')
}
} catch (error) {
console.warn('Failed to restore state:', error)
// Reset to default state
context.store.$reset()
}
},
afterRestore: (context) => {
console.log('State restored successfully')
}
}
})
3. Privacy Considerations
ts
// stores/privacy-aware.ts
export const usePrivacyAwareStore = defineStore('privacyAware', {
state: () => ({
publicData: {},
sensitiveData: {},
userConsent: false
}),
persist: {
// Only persist if user consented
enabled: (store) => store.userConsent,
// Exclude sensitive data
paths: ['publicData', 'userConsent']
}
})
Troubleshooting
Common Issues
Storage Quota Exceeded
ts// Handle quota exceeded errors const safeStorage = { setItem: (key: string, value: string) => { try { localStorage.setItem(key, value) } catch (error) { if (error.name === 'QuotaExceededError') { // Clear old data or compress clearOldData() localStorage.setItem(key, value) } } } }
Circular References
ts// Use custom serializer to handle circular references const circularSafeSerializer = { serialize: (value: any) => { const seen = new WeakSet() return JSON.stringify(value, (key, val) => { if (typeof val === 'object' && val !== null) { if (seen.has(val)) { return '[Circular]' } seen.add(val) } return val }) }, deserialize: JSON.parse }
Browser Compatibility
ts// Feature detection const isStorageAvailable = (type: 'localStorage' | 'sessionStorage') => { try { const storage = window[type] const test = '__storage_test__' storage.setItem(test, test) storage.removeItem(test) return true } catch { return false } }