Kurs Vue.js · Moduł 11: Pinia

Actions w Pinia

3 min czytania
W tej lekcji4

Actions to funkcje modyfikujące state i wykonujące operacje - jak komendy kontroli misji NOVA LAB. To tu znajduje się logika biznesowa: pobieranie danych, walidacja, obsługa błędów.

Podstawowe actions

W Composition API actions to zwykłe funkcje:

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  // Prosta action
9  function selectMember(member) {
10    selectedMember.value = member
11  }
12
13  // Action z logiką
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})

Asynchroniczne actions

Actions mogą być asynchroniczne - idealne do pobierania danych z 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})

Obsługa błędów w actions

Wzorzec try/catch/finally jest standardem w asynchronicznych actions:

1async function saveData(data) {
2  isLoading.value = true
3  error.value = null
4
5  try {
6    const result = await apiCall(data)
7    // Sukces - aktualizuj state
8    items.value.push(result)
9    return result
10  } catch (e) {
11    // Błąd - zapisz komunikat
12    error.value = e.message
13    throw e // Przekaż błąd do komponentu
14  } finally {
15    // Zawsze - wyłącz loading
16    isLoading.value = false
17  }
18}

Wywoływanie actions z komponentu

1<script setup>
2import { onMounted } from 'vue'
3import { useCrewStore } from '@/stores/crew'
4
5const crewStore = useCrewStore()
6
7// Wywołanie przy montowaniu
8onMounted(() => {
9  crewStore.fetchCrew()
10})
11
12// Handler z obsługą błędu
13async function handleAddMember(member) {
14  try {
15    await crewStore.addMember(member)
16  } catch (e) {
17    alert('Error: ' + e.message)
18  }
19}
20</script>
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Symulacja store załogi z actions
5const members = ref([
6  { id: 1, name: 'Komandor Nova', role: 'komandor', status: 'active' },
7  { id: 2, name: 'Inżynier Atlas', role: 'inżynier', status: 'active' },
8  { id: 3, name: 'Naukowiec Darwin', role: 'naukowiec', status: 'resting' }
9])
10const isLoading = ref(false)
11const error = ref(null)
12const newName = ref('')
13const newRole = ref('inżynier')
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: 'naukowiec',
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 = 'Imię jest wymagane'
43    return
44  }
45  if (members.value.length >= 8) {
46    error.value = 'Osiągnięto maksymalny rozmiar załogi (8)!'
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 - demo actions</h1>
74
75    <div class="stats">
76      <span>Załoga: {{ crewCount }}/8</span>
77      <span>Aktywni: {{ activeMembers.length }}</span>
78    </div>
79
80    <div class="add-form">
81      <input v-model="newName" placeholder="Imię członka załogi..." class="input" />
82      <select v-model="newRole" class="input">
83        <option value="komandor">Komandor</option>
84        <option value="inżynier">Inżynier</option>
85        <option value="naukowiec">Naukowiec</option>
86      </select>
87      <button @click="addMember" class="btn">Dodaj członka</button>
88      <button @click="fetchCrew" :disabled="isLoading" class="btn btn-alt">
89        {{ isLoading ? 'Ładowanie...' : 'Pobierz z 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' ? 'Odpocznij' : 'Aktywuj' }}
106          </button>
107          <button @click="removeMember(member.id)" class="btn-sm btn-danger">
108            Usuń
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>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym są actions w Pinia?

  2. 2. Czy actions w Pinia mogą być asynchroniczne?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Dodaj async action fetchCrew (try/catch/finally) i action removeMember.

  • Układanie w pionie

    Ułóż wzorzec async action z obsługą błędów:

  • Edytor kodu

    Stwórz addMember z walidacją (max 4) i fetchWithError symulujący błąd API.

  • Klikanie w kolejności

    Ułóż kroki używania store w <script setup>:

  • Układanie w pionie

    Uporządkuj kroki wykonywania async action:

Przydatne artykuły