Cours Vue.js · Module 11: Pinia
Les Actions dans Pinia
Dans cette leçon4
Les actions sont des fonctions qui modifient le state et effectuent des opérations - comme les commandes du centre de contrôle de mission du NOVA LAB. C'est là que vit la logique métier : récupération de données, validation, gestion des erreurs.
Actions de base
En Composition API, les actions sont de simples fonctions :
1import { defineStore } from 'pinia'
2import { ref } from 'vue'
3
4export const useCrewStore = defineStore('crew', () => {
5 const members = ref([])
6 const selectedMember = ref(null)
7
8 // Action simple
9 function selectMember(member) {
10 selectedMember.value = member
11 }
12
13 // Action avec logique
14 function addMember(member) {
15 if (members.value.length >= 8) {
16 throw new Error('Maximum crew size reached!')
17 }
18 members.value.push({
19 ...member,
20 id: Date.now(),
21 joinedAt: new Date()
22 })
23 }
24
25 function removeMember(id) {
26 members.value = members.value.filter(m => m.id !== id)
27 if (selectedMember.value?.id === id) {
28 selectedMember.value = null
29 }
30 }
31
32 return { members, selectedMember, selectMember, addMember, removeMember }
33})Actions asynchrones
Les actions peuvent être asynchrones - idéal pour récupérer des données depuis une API :
1export const useCrewStore = defineStore('crew', () => {
2 const members = ref([])
3 const isLoading = ref(false)
4 const error = ref(null)
5
6 async function fetchCrew() {
7 isLoading.value = true
8 error.value = null
9
10 try {
11 const response = await fetch('/api/crew')
12 if (!response.ok) {
13 throw new Error('Failed to fetch crew data')
14 }
15 members.value = await response.json()
16 } catch (e) {
17 error.value = e.message
18 } finally {
19 isLoading.value = false
20 }
21 }
22
23 async function updateMemberRole(id, newRole) {
24 try {
25 const response = await fetch(`/api/crew/${id}`, {
26 method: 'PATCH',
27 headers: { 'Content-Type': 'application/json' },
28 body: JSON.stringify({ role: newRole })
29 })
30
31 if (!response.ok) throw new Error('Update failed')
32
33 const updated = await response.json()
34 const index = members.value.findIndex(m => m.id === id)
35 if (index !== -1) {
36 members.value[index] = updated
37 }
38 } catch (e) {
39 error.value = e.message
40 throw e
41 }
42 }
43
44 return { members, isLoading, error, fetchCrew, updateMemberRole }
45})Gestion des erreurs dans les actions
Le motif try/catch/finally est standard dans les actions asynchrones :
1async function saveData(data) {
2 isLoading.value = true
3 error.value = null
4
5 try {
6 const result = await apiCall(data)
7 // Succès - on met à jour le state
8 items.value.push(result)
9 return result
10 } catch (e) {
11 // Erreur - on sauvegarde le message
12 error.value = e.message
13 throw e // On propage l'erreur au composant
14 } finally {
15 // Toujours - on désactive le chargement
16 isLoading.value = false
17 }
18}Appeler des actions depuis un composant
1<script setup>
2import { onMounted } from 'vue'
3import { useCrewStore } from '@/stores/crew'
4
5const crewStore = useCrewStore()
6
7// Appel au montage
8onMounted(() => {
9 crewStore.fetchCrew()
10})
11
12// Handler avec gestion des erreurs
13async function handleAddMember(member) {
14 try {
15 await crewStore.addMember(member)
16 } catch (e) {
17 alert('Error: ' + e.message)
18 }
19}
20</script>Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Simulation d'un Crew Store avec actions
5const members = ref([
6 { id: 1, name: 'Commander Nova', role: 'commander', status: 'active' },
7 { id: 2, name: 'Engineer Atlas', role: 'engineer', status: 'active' },
8 { id: 3, name: 'Scientist Darwin', role: 'scientist', status: 'resting' }
9])
10const isLoading = ref(false)
11const error = ref(null)
12const newName = ref('')
13const newRole = ref('engineer')
14
15// Getters
16const activeMembers = computed(() =>
17 members.value.filter(m => m.status === 'active')
18)
19const crewCount = computed(() => members.value.length)
20
21// Actions
22async function fetchCrew() {
23 isLoading.value = true
24 error.value = null
25 try {
26 await new Promise(r => setTimeout(r, 1000))
27 members.value.push({
28 id: Date.now(),
29 name: 'Dr. Mars ' + Math.floor(Math.random() * 100),
30 role: 'scientist',
31 status: 'active'
32 })
33 } catch (e) {
34 error.value = e.message
35 } finally {
36 isLoading.value = false
37 }
38}
39
40function addMember() {
41 if (!newName.value.trim()) {
42 error.value = 'Name is required'
43 return
44 }
45 if (members.value.length >= 8) {
46 error.value = 'Maximum crew size (8) reached!'
47 return
48 }
49 error.value = null
50 members.value.push({
51 id: Date.now(),
52 name: newName.value,
53 role: newRole.value,
54 status: 'active'
55 })
56 newName.value = ''
57}
58
59function removeMember(id) {
60 members.value = members.value.filter(m => m.id !== id)
61}
62
63function toggleStatus(id) {
64 const member = members.value.find(m => m.id === id)
65 if (member) {
66 member.status = member.status === 'active' ? 'resting' : 'active'
67 }
68}
69</script>
70
71<template>
72 <div class="nova-lab">
73 <h1>NOVA LAB - Actions Demo</h1>
74
75 <div class="stats">
76 <span>Crew: {{ crewCount }}/8</span>
77 <span>Active: {{ activeMembers.length }}</span>
78 </div>
79
80 <div class="add-form">
81 <input v-model="newName" placeholder="Member name..." class="input" />
82 <select v-model="newRole" class="input">
83 <option value="commander">Commander</option>
84 <option value="engineer">Engineer</option>
85 <option value="scientist">Scientist</option>
86 </select>
87 <button @click="addMember" class="btn">Add Member</button>
88 <button @click="fetchCrew" :disabled="isLoading" class="btn btn-alt">
89 {{ isLoading ? 'Loading...' : 'Fetch From API' }}
90 </button>
91 </div>
92
93 <div v-if="error" class="error">{{ error }}</div>
94
95 <div class="crew-list">
96 <div v-for="member in members" :key="member.id"
97 class="crew-card" :class="member.status">
98 <div class="crew-info">
99 <strong>{{ member.name }}</strong>
100 <span class="role">{{ member.role }}</span>
101 <span class="status-badge">{{ member.status }}</span>
102 </div>
103 <div class="crew-actions">
104 <button @click="toggleStatus(member.id)" class="btn-sm">
105 {{ member.status === 'active' ? 'Rest' : 'Activate' }}
106 </button>
107 <button @click="removeMember(member.id)" class="btn-sm btn-danger">
108 Remove
109 </button>
110 </div>
111 </div>
112 </div>
113 </div>
114</template>
115
116<style scoped>
117.nova-lab {
118 background: #0a0e27;
119 color: #00ff88;
120 min-height: 100vh;
121 padding: 20px;
122 font-family: monospace;
123}
124h1 { color: #00ff88; margin-bottom: 15px; text-align: center; }
125.stats { text-align: center; margin-bottom: 15px; color: #00b4d8; }
126.stats span { margin: 0 10px; }
127.add-form { display: flex; gap: 8px; margin-bottom: 15px; flex-wrap: wrap; max-width: 600px; margin: 0 auto 15px; }
128.input {
129 background: #0a0e27;
130 border: 2px solid #00b4d8;
131 color: #00ff88;
132 padding: 8px;
133 font-family: monospace;
134 flex: 1;
135 min-width: 120px;
136}
137.btn {
138 background: #00b4d8;
139 color: #0a0e27;
140 border: none;
141 padding: 8px 16px;
142 cursor: pointer;
143 font-family: monospace;
144 font-weight: bold;
145}
146.btn:hover { background: #00ff88; }
147.btn:disabled { opacity: 0.5; cursor: not-allowed; }
148.btn-alt { background: #00ff88; }
149.error {
150 background: rgba(255, 68, 68, 0.1);
151 border: 2px solid #ff4444;
152 color: #ff4444;
153 padding: 10px;
154 margin-bottom: 15px;
155 max-width: 600px;
156 margin: 0 auto 15px;
157}
158.crew-list { max-width: 600px; margin: 0 auto; }
159.crew-card {
160 display: flex;
161 justify-content: space-between;
162 align-items: center;
163 padding: 12px;
164 margin-bottom: 8px;
165 border: 2px solid #00ff88;
166 background: rgba(0, 255, 136, 0.05);
167 flex-wrap: wrap;
168 gap: 8px;
169}
170.crew-card.resting { border-color: #ff9900; opacity: 0.7; }
171.crew-info { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
172.role { color: #00b4d8; }
173.status-badge {
174 padding: 2px 8px;
175 font-size: 0.8em;
176 background: rgba(0, 255, 136, 0.2);
177 color: #00ff88;
178}
179.crew-card.resting .status-badge { background: rgba(255, 153, 0, 0.2); color: #ff9900; }
180.crew-actions { display: flex; gap: 5px; }
181.btn-sm {
182 background: #00b4d8;
183 color: #0a0e27;
184 border: none;
185 padding: 4px 10px;
186 cursor: pointer;
187 font-family: monospace;
188 font-size: 0.85em;
189}
190.btn-danger { background: #ff4444; color: #fff; }
191</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. Que sont les actions dans Pinia ?
2. Comment définit-on une action asynchrone dans Pinia ?
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 actions asynchrones avec des appels API
- Tri vertical
Ordonne les étapes du motif d'action asynchrone
- Éditeur de code
Implémente la gestion des erreurs dans des actions asynchrones
- Clique dans l'ordre
Ordonne les éléments pour utiliser correctement un store dans un composant
- Tri vertical
Ordonne les étapes du déroulement de l'exécution d'une action asynchrone