Skip to content

State Management

Current application setup comes with prebuilt state management library Pinia.

It is a recommended. It is type-safe, extensible, and modular by design.

This article will show how to enhance Pinia stores with configurable persistence and rehydration.

  1. Create a Pinia plugin for persisted state.
@/plugins/pinia.ts
ts
import type { PiniaPlugin, PiniaPluginContext } from 'pinia'

export interface PiniaPersistOptions {
  include?: string[]
  exclude?: string[]
  debug?: boolean
}

declare module 'pinia' {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  export interface DefineStoreOptionsBase<S, Store> {
    persist?: PiniaPersistOptions
  }
}

export function createPiniaLocalStoragePlugin(): PiniaPlugin {
  return (context: PiniaPluginContext) => {
    const store = context.store
    const key = store.$id
    const PiniaPersistOptions = context.options.persist as PiniaPersistOptions
    const debug = PiniaPersistOptions?.debug || false

    if (debug)
      console.log(`Creating PiniaLocalStoragePlugin for ${key} with options:`, PiniaPersistOptions)

    function filterState(state: Record<string, unknown>): Record<string, unknown> {
      const include = PiniaPersistOptions?.include || []
      const exclude = PiniaPersistOptions?.exclude || []
      if (include.length > 0) {
        return Object.fromEntries(Object.entries(state).filter(([key]) => include.includes(key)))
      }
      if (exclude.length > 0) {
        return Object.fromEntries(Object.entries(state).filter(([key]) => !exclude.includes(key)))
      }
      return state
    }

    const savedState = localStorage.getItem(key)
    if (savedState) {
      try {
        const parsedState = JSON.parse(savedState)
        store.$patch(parsedState)
        if (debug) console.log(`Restored ${key} from localStorage:`, parsedState)
      } catch (error) {
        console.error(`Error parsing localStorage for ${key}:`, error)
      }
    }

    store.$subscribe((mutation, state) => {
      if (!PiniaPersistOptions) return
      try {
        const filteredState = filterState(state)
        localStorage.setItem(key, JSON.stringify(filteredState))
        if (debug) console.log(`Saved ${store.$id} to localStorage:`, filteredState)
      } catch (error) {
        console.error(`Error saving ${key} to localStorage:`, error)
      }
    })
  }
}
  1. Modify @/main.ts to add persist plugin to pinia.
ts
// ...
import { createPiniaLocalStoragePlugin } from './plugins/pinia'
// ...
app.use(createPinia().use(createPiniaLocalStoragePlugin()))
// ...
  1. Create app store @/stores/settings.ts
ts
import { defineStore, acceptHMRUpdate } from 'pinia'

export const useSettingsStore = defineStore(
  'settings',
  () => {
    const setTheme = useTheme()

    const theme = ref('light')

    function themeToggle() {
      theme.value = theme.value === 'light' ? 'dark' : 'light'
      setTheme.global.name.value = theme.value
    }

    const themeIcon = computed(() => {
      if (setTheme.global.name.value === 'light') return '$mdiWeatherSunny'
      if (setTheme.global.name.value === 'dark') return '$mdiWeatherNight'
    })

    function init() {
      setTheme.global.name.value = theme.value
    }

    return { theme, themeToggle, themeIcon, init }
  },
  {
    persist: {
      include: ['theme'],
    },
  },
)

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useSettingsStore, import.meta.hot))
}
  1. Modify @/pages/sandbox/index.vue to use store for persisting theme - after closing and reopening browser theme is as it was set before
vue
<template>
  <v-card :style="cardBackground('#00AA00')">
    <v-card-title><v-icon icon="$mdiHome" />{{ t('sandbox.title') }}</v-card-title>
    <v-card-text>{{ t('sandbox.content') }}</v-card-text>
    <v-card-text>{{ t('sandbox.missing') }}</v-card-text>
    <v-card-actions>
      <v-btn color="primary">Primary</v-btn>
      <v-btn color="secondary">Secondary</v-btn>
      <v-spacer></v-spacer>
      <v-btn :prepend-icon="settings.themeIcon" @click="settings.themeToggle()">Toggle theme</v-btn>
    </v-card-actions>
  </v-card>
</template>

<script setup lang="ts">
const cardBackground = useCardBackground
const { t } = useI18n()
import { useSettingsStore } from '@/stores/settings';
const settings = useSettingsStore()
onMounted(() => {
  settings.init()
})
</script>