Vue.js course Β· Module 11: Pinia

Plugins and Persistence

2 min read
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-persistedstate

Configuration:

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. 1. What does the $subscribe method do?

  2. 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

Useful articles