Vue.js course Β· Module 11: Pinia

Actions in Pinia

3 min read
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. 1. What are actions in Pinia?

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

Useful articles