Vue.js course Β· Module 11: Pinia
Plugins and Persistence
In this lesson6
The NOVA LAB backup system ensures that critical data survives even a station restart. In Pinia, this role is fulfilled by plugins - extensions that add new capabilities to stores. The most important one is persistence - saving state to localStorage.
$subscribe - Listening for Changes
$subscribe allows reacting to every state change:
1const store = useResourceStore()
2
3store.$subscribe((mutation, state) => {
4 console.log('Store changed:', mutation.storeId)
5 console.log('Type:', mutation.type) // 'direct' | '$patch'
6 console.log('New state:', state)
7
8 // Automatic save to localStorage
9 localStorage.setItem('resources', JSON.stringify(state))
10})Subscribe options:
1store.$subscribe(
2 (mutation, state) => { /* ... */ },
3 { detached: true } // Survives component unmount
4)$onAction - Observing Actions
1const store = useResourceStore()
2
3store.$onAction(({ name, args, after, onError }) => {
4 console.log(`Action started: ${name}`, args)
5
6 after((result) => {
7 console.log(`Action ${name} completed`, result)
8 })
9
10 onError((error) => {
11 console.error(`Action ${name} failed`, error)
12 })
13})Persistence Plugin
The most popular plugin - pinia-plugin-persistedstate:
1npm install pinia-plugin-persistedstateConfiguration:
1// main.js
2import { createPinia } from 'pinia'
3import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
4
5const pinia = createPinia()
6pinia.use(piniaPluginPersistedstate)Usage in a store:
1export const useSettingsStore = defineStore('settings', () => {
2 const theme = ref('dark')
3 const language = ref('en')
4
5 return { theme, language }
6}, {
7 persist: true // Entire state to localStorage
8})Advanced Persist Configuration
1export const useAuthStore = defineStore('auth', () => {
2 const token = ref(null)
3 const user = ref(null)
4 const tempData = ref(null) // We don't want to save this
5
6 return { token, user, tempData }
7}, {
8 persist: {
9 // Only selected fields
10 paths: ['token'],
11
12 // Custom storage
13 storage: sessionStorage,
14
15 // Custom key
16 key: 'nova-lab-auth'
17 }
18})Creating a Custom Plugin
A Pinia plugin is a function that receives a context with the store:
1// plugins/logger.js
2export function loggerPlugin({ store }) {
3 store.$subscribe((mutation, state) => {
4 console.log(`[${mutation.storeId}] ${mutation.type}`)
5 })
6
7 store.$onAction(({ name, store: s, args }) => {
8 console.log(`[${s.$id}] Action: ${name}`, args)
9 })
10}
11
12// main.js
13const pinia = createPinia()
14pinia.use(loggerPlugin)Plugin Adding Properties
1function timestampPlugin({ store }) {
2 // Add lastUpdated to every store
3 store.lastUpdated = ref(null)
4
5 store.$subscribe(() => {
6 store.lastUpdated = new Date()
7 })
8}Code for this lesson: App.vue
1<script setup>
2import { ref, computed, watch } from 'vue'
3
4// Store simulation with $subscribe and plugins
5const settings = ref({
6 theme: 'dark',
7 language: 'pl',
8 fontSize: 14
9})
10
11const changeLog = ref([])
12const actionLog = ref([])
13
14// $subscribe simulation
15watch(settings, (newVal) => {
16 changeLog.value.unshift({
17 time: new Date().toLocaleTimeString(),
18 change: JSON.stringify(newVal)
19 })
20 if (changeLog.value.length > 6) changeLog.value.pop()
21
22 // Persist simulation - save to localStorage
23 try {
24 localStorage.setItem('nova-settings', JSON.stringify(newVal))
25 } catch (e) {}
26}, { deep: true })
27
28// $onAction simulation (logger plugin)
29function logAction(name, args = []) {
30 actionLog.value.unshift({
31 time: new Date().toLocaleTimeString(),
32 action: name,
33 args: args.length ? JSON.stringify(args) : '-'
34 })
35 if (actionLog.value.length > 6) actionLog.value.pop()
36}
37
38// Actions
39function toggleTheme() {
40 settings.value.theme = settings.value.theme === 'dark' ? 'light' : 'dark'
41 logAction('toggleTheme')
42}
43
44function setLanguage(lang) {
45 settings.value.language = lang
46 logAction('setLanguage', [lang])
47}
48
49function changeFontSize(delta) {
50 settings.value.fontSize = Math.max(10, Math.min(24, settings.value.fontSize + delta))
51 logAction('changeFontSize', [delta])
52}
53
54function resetSettings() {
55 settings.value = { theme: 'dark', language: 'pl', fontSize: 14 }
56 logAction('resetSettings')
57}
58
59const persistedData = computed(() => {
60 try {
61 return localStorage.getItem('nova-settings') || 'No data'
62 } catch (e) {
63 return 'Storage not available'
64 }
65})
66</script>
67
68<template>
69 <div class="nova-lab" :class="settings.theme" :style="{ fontSize: settings.fontSize + 'px' }">
70 <h1>NOVA LAB - Plugins & Persistence</h1>
71
72 <div class="grid">
73 <div class="panel">
74 <h2>Settings Store</h2>
75 <div class="setting">
76 <span>Theme:</span>
77 <button @click="toggleTheme" class="btn">{{ settings.theme }}</button>
78 </div>
79 <div class="setting">
80 <span>Language:</span>
81 <button @click="setLanguage('pl')" class="btn" :class="{ active: settings.language === 'pl' }">PL</button>
82 <button @click="setLanguage('en')" class="btn" :class="{ active: settings.language === 'en' }">EN</button>
83 </div>
84 <div class="setting">
85 <span>Font:</span>
86 <button @click="changeFontSize(-2)" class="btn">A-</button>
87 <span>{{ settings.fontSize }}px</span>
88 <button @click="changeFontSize(2)" class="btn">A+</button>
89 </div>
90 <button @click="resetSettings" class="btn btn-reset">Reset All</button>
91 </div>
92
93 <div class="panel">
94 <h2>$subscribe Log</h2>
95 <div v-for="(entry, i) in changeLog" :key="i" class="log-entry">
96 <span class="time">{{ entry.time }}</span>
97 <span class="change">{{ entry.change }}</span>
98 </div>
99 <p v-if="!changeLog.length" class="empty">No changes yet</p>
100 </div>
101
102 <div class="panel">
103 <h2>$onAction Log (plugin)</h2>
104 <div v-for="(entry, i) in actionLog" :key="i" class="log-entry">
105 <span class="time">{{ entry.time }}</span>
106 <span class="action-name">{{ entry.action }}</span>
107 <span class="args">{{ entry.args }}</span>
108 </div>
109 <p v-if="!actionLog.length" class="empty">No actions yet</p>
110 </div>
111
112 <div class="panel">
113 <h2>Persisted (localStorage)</h2>
114 <pre class="persisted">{{ persistedData }}</pre>
115 </div>
116 </div>
117 </div>
118</template>
119
120<style scoped>
121.nova-lab {
122 min-height: 100vh;
123 padding: 20px;
124 font-family: monospace;
125 transition: all 0.3s;
126}
127.nova-lab.dark { background: #0a0e27; color: #00ff88; }
128.nova-lab.light { background: #f0f4f8; color: #1a1e3f; }
129h1 { margin-bottom: 20px; text-align: center; }
130h2 { color: #00b4d8; margin-bottom: 10px; font-size: 1em; }
131.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; max-width: 700px; margin: 0 auto; }
132.panel {
133 background: rgba(0, 180, 216, 0.1);
134 border: 2px solid #00b4d8;
135 padding: 12px;
136}
137.setting { display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }
138.btn {
139 background: #00b4d8;
140 color: #0a0e27;
141 border: none;
142 padding: 4px 12px;
143 cursor: pointer;
144 font-family: monospace;
145}
146.btn:hover { background: #00ff88; }
147.btn.active { background: #00ff88; }
148.btn-reset { background: #ff9900; margin-top: 10px; }
149.log-entry {
150 padding: 4px 0;
151 border-bottom: 1px solid rgba(0, 180, 216, 0.2);
152 font-size: 0.8em;
153 display: flex;
154 gap: 8px;
155 flex-wrap: wrap;
156}
157.time { color: #00b4d8; }
158.change { color: #00ff88; word-break: break-all; }
159.action-name { color: #ff9900; }
160.args { color: #666; }
161.empty { color: #666; font-style: italic; font-size: 0.85em; }
162.persisted {
163 background: #0a0e27;
164 color: #00ff88;
165 padding: 8px;
166 font-size: 0.75em;
167 word-break: break-all;
168 white-space: pre-wrap;
169}
170</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the $subscribe method do?
2. What does the pinia-plugin-persistedstate do?
These are 2 of 6 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Implement state monitoring and persistence
- Vertical ordering
Arrange the steps for configuring the persistence plugin
- Vertical ordering
Arrange the stages of the $subscribe execution flow
- Code editor
Build a complete logging system using $subscribe and $onAction
- Click in order
Arrange the elements of a Pinia plugin pattern
- Vertical ordering
Arrange the elements of advanced persistence configuration
- Horizontal ordering
Arrange the elements to access state in a store
- Vertical ordering
Arrange the stages of data flow in Pinia
- Horizontal ordering
Arrange the elements to use $patch with a function
- Click in order
Arrange the elements of the full store lifecycle
- Vertical ordering
Arrange the criteria for choosing Pinia vs provide/inject
- Vertical ordering
Arrange the steps for registering a Pinia plugin