Cours Vue.js · Module 11: Pinia
Composition de stores
Dans cette leçon3
Au NOVA LAB, les différents systèmes de la station doivent coopérer - le système d'oxygène communique avec le système d'alarme, et le système de navigation avec le système de communication. Il en va de même dans Pinia - les stores peuvent importer et utiliser d'autres stores.
Un store qui utilise un autre store
1// stores/auth.js
2import { defineStore } from 'pinia'
3import { ref, computed } from 'vue'
4
5export const useAuthStore = defineStore('auth', () => {
6 const user = ref(null)
7 const token = ref(null)
8
9 const isAuthenticated = computed(() => !!token.value)
10 const userName = computed(() => user.value?.name || 'Guest')
11
12 async function login(credentials) {
13 const res = await fetch('/api/login', {
14 method: 'POST',
15 headers: { 'Content-Type': 'application/json' },
16 body: JSON.stringify(credentials)
17 })
18 const data = await res.json()
19 token.value = data.token
20 user.value = data.user
21 }
22
23 function logout() {
24 user.value = null
25 token.value = null
26 }
27
28 return { user, token, isAuthenticated, userName, login, logout }
29})1// stores/missions.js - utilise le store auth
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4import { useAuthStore } from './auth'
5
6export const useMissionsStore = defineStore('missions', () => {
7 const missions = ref([])
8
9 async function fetchMissions() {
10 // On utilise le store auth pour récupérer le token
11 const authStore = useAuthStore()
12
13 if (!authStore.isAuthenticated) {
14 throw new Error('Must be logged in')
15 }
16
17 const res = await fetch('/api/missions', {
18 headers: {
19 'Authorization': `Bearer ${authStore.token}`
20 }
21 })
22
23 missions.value = await res.json()
24 }
25
26 return { missions, fetchMissions }
27})Dépendances mutuelles entre stores
Les stores peuvent s'utiliser mutuellement, mais appelle useStore() à l'intérieur des actions (et non en haut de la fonction setup) pour éviter les problèmes de dépendance circulaire :
1// stores/notifications.js
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4
5export const useNotificationStore = defineStore('notifications', () => {
6 const notifications = ref([])
7
8 function add(message, type = 'info') {
9 notifications.value.push({
10 id: Date.now(),
11 message,
12 type,
13 timestamp: new Date()
14 })
15 }
16
17 function remove(id) {
18 notifications.value = notifications.value.filter(n => n.id !== id)
19 }
20
21 return { notifications, add, remove }
22})
23
24// stores/resources.js - utilise notifications
25import { useNotificationStore } from './notifications'
26
27export const useResourceStore = defineStore('resources', () => {
28 const oxygen = ref(100)
29
30 function consumeOxygen(amount) {
31 oxygen.value -= amount
32
33 if (oxygen.value < 20) {
34 // Appelle useStore À L'INTÉRIEUR de l'action
35 const notifStore = useNotificationStore()
36 notifStore.add('Oxygen level critical!', 'danger')
37 }
38 }
39
40 return { oxygen, consumeOxygen }
41})Architecture multi-stores
Il est recommandé de créer beaucoup de petits stores spécialisés plutôt qu'un seul gros store :
1stores/
2├── auth.js # Authentification utilisateur
3├── resources.js # Ressources de la station (oxygène, eau, énergie)
4├── crew.js # Données de l'équipage
5├── missions.js # Missions et tâches
6├── notifications.js # Système de notifications
7└── settings.js # Paramètres de l'applicationChaque store est responsable d'un « module » de l'application - tout comme chaque module de la station NOVA LAB a sa propre fonction.
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Store composition - AuthStore
5const user = ref({ name: 'Commander Nova', role: 'admin' })
6const token = ref('jwt-token-xyz')
7const isAuthenticated = computed(() => !!token.value)
8
9// Store composition - ResourceStore (utilise AuthStore)
10const resources = ref([
11 { id: 1, name: 'Oxygen', level: 92, unit: '%' },
12 { id: 2, name: 'Water', level: 78, unit: '%' },
13 { id: 3, name: 'Energy', level: 65, unit: '%' }
14])
15
16// Store composition - NotificationStore
17const notifications = ref([])
18function addNotification(message, type = 'info') {
19 const id = Date.now()
20 notifications.value.push({ id, message, type, time: new Date().toLocaleTimeString() })
21 setTimeout(() => {
22 notifications.value = notifications.value.filter(n => n.id !== id)
23 }, 4000)
24}
25
26// Les actions de ResourceStore utilisent NotificationStore
27function consumeResource(id, amount) {
28 const resource = resources.value.find(r => r.id === id)
29 if (!resource) return
30
31 resource.level = Math.max(0, resource.level - amount)
32
33 if (resource.level < 20) {
34 // Inter-stores : resource -> notification
35 addNotification(`${resource.name} critically low: ${resource.level}${resource.unit}!`, 'danger')
36 } else if (resource.level < 50) {
37 addNotification(`${resource.name} below 50%: ${resource.level}${resource.unit}`, 'warning')
38 }
39}
40
41function refillResource(id) {
42 const resource = resources.value.find(r => r.id === id)
43 if (resource) {
44 resource.level = 100
45 addNotification(`${resource.name} refilled to 100%`, 'success')
46 }
47}
48
49// Getter inter-stores
50const criticalResources = computed(() =>
51 resources.value.filter(r => r.level < 30)
52)
53</script>
54
55<template>
56 <div class="nova-lab">
57 <h1>NOVA LAB - Store Composition</h1>
58
59 <div class="user-bar" v-if="isAuthenticated">
60 Logged in: {{ user.name }} ({{ user.role }})
61 </div>
62
63 <div class="resources-grid">
64 <div v-for="r in resources" :key="r.id"
65 class="resource-card" :class="{ critical: r.level < 20, warning: r.level < 50 }">
66 <h3>{{ r.name }}</h3>
67 <div class="level-bar">
68 <div class="level-fill" :style="{ width: r.level + '%' }"></div>
69 </div>
70 <div class="level-text">{{ r.level }}{{ r.unit }}</div>
71 <div class="r-actions">
72 <button @click="consumeResource(r.id, 15)" class="btn">-15</button>
73 <button @click="refillResource(r.id)" class="btn btn-green">Refill</button>
74 </div>
75 </div>
76 </div>
77
78 <div v-if="criticalResources.length" class="critical-alert">
79 CRITICAL: {{ criticalResources.map(r => r.name).join(', ') }}
80 </div>
81
82 <div class="notifications">
83 <div v-for="n in notifications" :key="n.id" class="notif" :class="n.type">
84 {{ n.time }} - {{ n.message }}
85 </div>
86 </div>
87 </div>
88</template>
89
90<style scoped>
91.nova-lab {
92 background: #0a0e27;
93 color: #00ff88;
94 min-height: 100vh;
95 padding: 20px;
96 font-family: monospace;
97}
98h1 { color: #00ff88; margin-bottom: 15px; text-align: center; }
99.user-bar {
100 text-align: center;
101 padding: 8px;
102 background: rgba(0, 180, 216, 0.1);
103 border: 1px solid #00b4d8;
104 margin-bottom: 15px;
105 color: #00b4d8;
106}
107.resources-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 15px; }
108.resource-card {
109 background: rgba(0, 255, 136, 0.05);
110 border: 2px solid #00ff88;
111 padding: 15px;
112 text-align: center;
113}
114.resource-card.warning { border-color: #ff9900; }
115.resource-card.critical { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
116h3 { color: #00b4d8; margin-bottom: 8px; }
117.level-bar { height: 12px; background: #1a1e3f; border: 1px solid #00b4d8; margin: 8px 0; }
118.level-fill { height: 100%; background: #00ff88; transition: width 0.3s; }
119.resource-card.warning .level-fill { background: #ff9900; }
120.resource-card.critical .level-fill { background: #ff4444; }
121.level-text { font-size: 1.2em; font-weight: bold; margin-bottom: 8px; }
122.r-actions { display: flex; gap: 5px; justify-content: center; }
123.btn {
124 background: #00b4d8;
125 color: #0a0e27;
126 border: none;
127 padding: 5px 12px;
128 cursor: pointer;
129 font-family: monospace;
130}
131.btn-green { background: #00ff88; }
132.critical-alert {
133 padding: 10px;
134 background: rgba(255, 68, 68, 0.2);
135 border: 2px solid #ff4444;
136 color: #ff4444;
137 text-align: center;
138 margin-bottom: 15px;
139 font-weight: bold;
140}
141.notifications { max-width: 500px; margin: 0 auto; }
142.notif {
143 padding: 8px;
144 margin: 4px 0;
145 border: 1px solid;
146 font-size: 0.9em;
147}
148.notif.info { border-color: #00b4d8; color: #00b4d8; }
149.notif.warning { border-color: #ff9900; color: #ff9900; }
150.notif.danger { border-color: #ff4444; color: #ff4444; }
151.notif.success { border-color: #00ff88; color: #00ff88; }
152</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. Comment un store Pinia peut-il utiliser un autre store ?
2. Où doit-on appeler useStore() lorsqu'on utilise des stores dans d'autres stores ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée des stores qui communiquent entre eux
- Tri vertical
Ordonne les couches d'une architecture multi-stores
- Clique dans l'ordre
Ordonne les éléments pour appeler une action d'un autre store
- Éditeur de code
Crée un dashboard utilisant plusieurs stores