Skip to content

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
  }
})

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

  1. 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)
          }
        }
      }
    }
  2. 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
    }
  3. 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
      }
    }

Released under the MIT License.