Kurs Vue.js · Moduł 11: Pinia

Podstawy Pinia

3 min czytania
W tej lekcji5

Witaj w Centralnej Bazie Danych NOVA LAB! Rok 2087 - tu przechowujemy wszystkie krytyczne dane misji marsjańskiej. Podobnie jak główny komputer bazy przechowuje informacje o systemach, załodze i zasobach, Pinia przechowuje globalny stan aplikacji Vue.

Czym jest Pinia?

Pinia to oficjalna biblioteka state management dla Vue 3. Jest następcą Vuex i oferuje znacznie prostszą składnię. Wyobraź sobie ją jako centralny komputer NOVA LAB - jeden punkt kontroli, z którego wszystkie moduły stacji pobierają dane.

Główne zalety Pinia:

  • Prostsza składnia niż Vuex (brak mutations!)
  • Pełne wsparcie TypeScript
  • Świetna integracja z Vue DevTools
  • Modułowa architektura - wiele niezależnych stores

Instalacja i konfiguracja

Aby uruchomić Pinia, potrzebujemy dwóch kroków - zainstalować pakiet i podłączyć go do aplikacji:

1npm install pinia
1// main.js
2import { createApp } from 'vue'
3import { createPinia } from 'pinia'
4import App from './App.vue'
5
6const pinia = createPinia()
7const app = createApp(App)
8
9app.use(pinia)
10app.mount('#app')

Funkcja createPinia() tworzy instancję Pinia, a app.use(pinia) rejestruje ją jako plugin Vue. Od tego momentu wszystkie stores są dostępne w całej aplikacji.

Definiowanie Store - defineStore

Store tworzymy za pomocą defineStore. Przyjmuje dwa argumenty: unikalną nazwę store i funkcję setup (Composition API style):

1// stores/mission.js
2import { defineStore } from 'pinia'
3import { ref, computed } from 'vue'
4
5export const useMissionStore = defineStore('mission', () => {
6  // State - dane reaktywne
7  const missionName = ref('Mars Exploration Alpha')
8  const status = ref('active')
9  const crewCount = ref(6)
10
11  // Getters - wartości wyliczane
12  const statusLabel = computed(() =>
13    status.value === 'active' ? 'AKTYWNA' : 'NIEAKTYWNA'
14  )
15
16  // Actions - funkcje modyfikujące state
17  function updateStatus(newStatus) {
18    status.value = newStatus
19  }
20
21  // Zwracamy wszystko co ma być dostępne
22  return {
23    missionName,
24    status,
25    crewCount,
26    statusLabel,
27    updateStatus
28  }
29})

Konwencja nazewnictwa: useXxxStore - prefix use i suffix Store.

Użycie store w komponencie

1<script setup>
2import { useMissionStore } from '@/stores/mission'
3
4const missionStore = useMissionStore()
5
6// Dostęp do state
7console.log(missionStore.missionName)
8
9// Wywołanie action
10missionStore.updateStatus('completed')
11</script>
12
13<template>
14  <div>
15    <h1>{{ missionStore.missionName }}</h1>
16    <p>Status: {{ missionStore.statusLabel }}</p>
17    <button @click="missionStore.updateStatus('paused')">
18      Wstrzymaj misję
19    </button>
20  </div>
21</template>

storeToRefs - reaktywna destrukturyzacja

Zwykła destrukturyzacja traci reaktywność. Użyj storeToRefs dla state i getters:

1<script setup>
2import { useMissionStore } from '@/stores/mission'
3import { storeToRefs } from 'pinia'
4
5const store = useMissionStore()
6
7// State i getters - storeToRefs (zachowuje reaktywność)
8const { missionName, status, statusLabel } = storeToRefs(store)
9
10// Actions - normalna destrukturyzacja (to funkcje, nie refs)
11const { updateStatus } = store
12</script>
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Symulacja store Pinia (w stylu Composition API)
5// W prawdziwej aplikacji: import { defineStore } from 'pinia'
6
7// State
8const missionName = ref('Eksploracja Marsa Alfa')
9const status = ref('active')
10const crewCount = ref(6)
11const fuelLevel = ref(87)
12
13// Getters
14const statusLabel = computed(() =>
15  status.value === 'active' ? 'AKTYWNA' : 'NIEAKTYWNA'
16)
17
18const fuelStatus = computed(() => {
19  if (fuelLevel.value > 70) return 'Optimal'
20  if (fuelLevel.value > 30) return 'Moderate'
21  return 'Critical'
22})
23
24// Actions
25function toggleStatus() {
26  status.value = status.value === 'active' ? 'paused' : 'active'
27}
28
29function consumeFuel(amount) {
30  fuelLevel.value = Math.max(0, fuelLevel.value - amount)
31}
32
33function refuel() {
34  fuelLevel.value = 100
35}
36</script>
37
38<template>
39  <div class="nova-lab">
40    <h1>NOVA LAB - podstawy Pinia</h1>
41
42    <div class="panel">
43      <h2>Store misji</h2>
44      <div class="stat">
45        <span class="label">Misja:</span>
46        <span>{{ missionName }}</span>
47      </div>
48      <div class="stat">
49        <span class="label">Status:</span>
50        <span :class="status">{{ statusLabel }}</span>
51      </div>
52      <div class="stat">
53        <span class="label">Załoga:</span>
54        <span>{{ crewCount }} osób</span>
55      </div>
56      <div class="stat">
57        <span class="label">Paliwo:</span>
58        <div class="fuel-bar">
59          <div class="fuel-fill" :style="{ width: fuelLevel + '%' }" :class="fuelStatus.toLowerCase()"></div>
60        </div>
61        <span>{{ fuelLevel }}% ({{ fuelStatus }})</span>
62      </div>
63
64      <div class="actions">
65        <button @click="toggleStatus" class="btn">
66          {{ status === 'active' ? 'Wstrzymaj' : 'Wznów' }}
67        </button>
68        <button @click="consumeFuel(10)" class="btn btn-warn">
69          Zużyj paliwo (-10)
70        </button>
71        <button @click="refuel" class="btn btn-success">
72          Zatankuj
73        </button>
74      </div>
75    </div>
76  </div>
77</template>
78
79<style scoped>
80.nova-lab {
81  background: #0a0e27;
82  color: #00ff88;
83  min-height: 100vh;
84  padding: 20px;
85  font-family: monospace;
86}
87h1 { color: #00ff88; margin-bottom: 20px; text-align: center; }
88.panel {
89  background: rgba(0, 180, 216, 0.1);
90  border: 2px solid #00b4d8;
91  padding: 20px;
92  max-width: 500px;
93  margin: 0 auto;
94}
95h2 { color: #00b4d8; margin-bottom: 15px; }
96.stat {
97  display: flex;
98  align-items: center;
99  gap: 10px;
100  margin: 10px 0;
101  flex-wrap: wrap;
102}
103.label { color: #00b4d8; min-width: 80px; }
104.active { color: #00ff88; font-weight: bold; }
105.paused { color: #ff9900; font-weight: bold; }
106.fuel-bar {
107  width: 150px;
108  height: 16px;
109  background: #1a1e3f;
110  border: 1px solid #00b4d8;
111}
112.fuel-fill {
113  height: 100%;
114  transition: width 0.3s;
115}
116.fuel-fill.optimal { background: #00ff88; }
117.fuel-fill.moderate { background: #ff9900; }
118.fuel-fill.critical { background: #ff4444; }
119.actions {
120  display: flex;
121  gap: 8px;
122  margin-top: 15px;
123  flex-wrap: wrap;
124}
125.btn {
126  background: #00b4d8;
127  color: #0a0e27;
128  border: none;
129  padding: 8px 16px;
130  cursor: pointer;
131  font-family: monospace;
132  font-weight: bold;
133}
134.btn:hover { background: #00ff88; }
135.btn-warn { background: #ff9900; }
136.btn-warn:hover { background: #ffbb33; }
137.btn-success { background: #00ff88; }
138.btn-success:hover { background: #33ffaa; }
139</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 jest Pinia w ekosystemie Vue 3?

  2. 2. Jak utworzyć instancję Pinia w main.js?

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

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz stan reaktywny (missionName, status, crewCount) i akcję toggleStatus.

  • Klikanie w kolejności

    Ułóż kod definiowania store o nazwie 'mission':

  • Edytor kodu

    Użyj storeToRefs do destrukturyzacji state i wywołaj action z template.

Przydatne artykuły