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.