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.
- 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)
}
})
}
}
- Modify
@/main.ts
to add persist plugin to pinia.
ts
// ...
import { createPiniaLocalStoragePlugin } from './plugins/pinia'
// ...
app.use(createPinia().use(createPiniaLocalStoragePlugin()))
// ...
- 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))
}
- 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>