Cours Vue.js · Module 11: Pinia

Plugins et persistance

2 min de lecture
Dans cette leçon6

Le système de sauvegarde du NOVA LAB garantit que les données critiques survivent même à un redémarrage de la station. Dans Pinia, ce rôle est rempli par les plugins - des extensions qui ajoutent de nouvelles capacités aux stores. Le plus important est la persistance - la sauvegarde de l'état dans le localStorage.

$subscribe - écouter les changements

$subscribe permet de réagir à chaque changement du state :

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  // Sauvegarde automatique dans le localStorage
9  localStorage.setItem('resources', JSON.stringify(state))
10})

Options de subscribe :

1store.$subscribe(
2  (mutation, state) => { /* ... */ },
3  { detached: true } // Survit au démontage du composant
4)

$onAction - observer les 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})

Plugin de persistance

Le plugin le plus populaire - 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)

Utilisation dans un 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 // Tout le state dans le localStorage
8})

Configuration avancée de persist

1export const useAuthStore = defineStore('auth', () => {
2  const token = ref(null)
3  const user = ref(null)
4  const tempData = ref(null) // On ne veut pas sauvegarder ceci
5
6  return { token, user, tempData }
7}, {
8  persist: {
9    // Seulement les champs sélectionnés
10    paths: ['token'],
11
12    // Stockage personnalisé
13    storage: sessionStorage,
14
15    // Clé personnalisée
16    key: 'nova-lab-auth'
17  }
18})

Créer un plugin personnalisé

Un plugin Pinia est une fonction qui reçoit un contexte contenant le 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 ajoutant des propriétés

1function timestampPlugin({ store }) {
2  // Ajoute lastUpdated à chaque store
3  store.lastUpdated = ref(null)
4
5  store.$subscribe(() => {
6    store.lastUpdated = new Date()
7  })
8}
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed, watch } from 'vue'
3
4// Simulation d'un store avec $subscribe et plugins
5const settings = ref({
6  theme: 'dark',
7  language: 'fr',
8  fontSize: 14
9})
10
11const changeLog = ref([])
12const actionLog = ref([])
13
14// Simulation de $subscribe
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  // Simulation de persist - sauvegarde dans localStorage
23  try {
24    localStorage.setItem('nova-settings', JSON.stringify(newVal))
25  } catch (e) {}
26}, { deep: true })
27
28// Simulation de $onAction (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: 'fr', 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('fr')" class="btn" :class="{ active: settings.language === 'fr' }">FR</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>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que fait la méthode $subscribe ?

  2. 2. Que fait le plugin pinia-plugin-persistedstate ?

Voici 2 des 6 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Implémente la surveillance du state et la persistance

  • Tri vertical

    Ordonne les étapes de configuration du plugin de persistance

  • Tri vertical

    Ordonne les étapes du déroulement de l'exécution de $subscribe

  • Éditeur de code

    Construis un système de journalisation complet avec $subscribe et $onAction

  • Clique dans l'ordre

    Ordonne les éléments d'un motif de plugin Pinia

  • Tri vertical

    Ordonne les éléments d'une configuration avancée de persistance

  • Tri horizontal

    Ordonne les éléments pour accéder au state dans un store

  • Tri vertical

    Ordonne les étapes du flux de données dans Pinia

  • Tri horizontal

    Ordonne les éléments pour utiliser $patch avec une fonction

  • Clique dans l'ordre

    Ordonne les éléments du cycle de vie complet d'un store

  • Tri vertical

    Ordonne les critères de choix entre Pinia et provide/inject

  • Tri vertical

    Ordonne les étapes pour enregistrer un plugin Pinia