Vue.js course Β· Module 11: Pinia
Actions in Pinia
In this lesson4
Actions are functions that modify state and perform operations - like NOVA LAB mission control commands. This is where business logic lives: fetching data, validation, error handling.
Basic Actions
In Composition API, actions are regular functions:
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 // Simple action
9 function selectMember(member) {
10 selectedMember.value = member
11 }
12
13 // Action with logic
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})Asynchronous Actions
Actions can be asynchronous - ideal for fetching data from an 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})Error Handling in Actions
The try/catch/finally pattern is standard in asynchronous actions:
1async function saveData(data) {
2 isLoading.value = true
3 error.value = null
4
5 try {
6 const result = await apiCall(data)
7 // Success - update state
8 items.value.push(result)
9 return result
10 } catch (e) {
11 // Error - save message
12 error.value = e.message
13 throw e // Pass error to component
14 } finally {
15 // Always - disable loading
16 isLoading.value = false
17 }
18}Calling Actions from a Component
1<script setup>
2import { onMounted } from 'vue'
3import { useCrewStore } from '@/stores/crew'
4
5const crewStore = useCrewStore()
6
7// Call on mount
8onMounted(() => {
9 crewStore.fetchCrew()
10})
11
12// Handler with error handling
13async function handleAddMember(member) {
14 try {
15 await crewStore.addMember(member)
16 } catch (e) {
17 alert('Error: ' + e.message)
18 }
19}
20</script>Code for this lesson: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Crew Store simulation with 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>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 are actions in Pinia?
2. How do you define an asynchronous action in Pinia?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create asynchronous actions with API calls
- Vertical ordering
Arrange the steps of the async action pattern
- Code editor
Implement error handling in asynchronous actions
- Click in order
Arrange the elements to correctly use a store in a component
- Vertical ordering
Arrange the stages of the async action execution flow