Kurs Vue.js · Moduł 11: Pinia

Kompozycja Stores

3 min czytania
W tej lekcji3

W NOVA LAB różne systemy stacji muszą ze sobą współpracować - system tlenu komunikuje się z systemem alarmowym, a system nawigacji z systemem komunikacji. Podobnie w Pinia - stores mogą importować i używać innych stores.

Store używający innego store

1// stores/auth.js
2import { defineStore } from 'pinia'
3import { ref, computed } from 'vue'
4
5export const useAuthStore = defineStore('auth', () => {
6  const user = ref(null)
7  const token = ref(null)
8
9  const isAuthenticated = computed(() => !!token.value)
10  const userName = computed(() => user.value?.name || 'Guest')
11
12  async function login(credentials) {
13    const res = await fetch('/api/login', {
14      method: 'POST',
15      headers: { 'Content-Type': 'application/json' },
16      body: JSON.stringify(credentials)
17    })
18    const data = await res.json()
19    token.value = data.token
20    user.value = data.user
21  }
22
23  function logout() {
24    user.value = null
25    token.value = null
26  }
27
28  return { user, token, isAuthenticated, userName, login, logout }
29})
1// stores/missions.js - używa auth store
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4import { useAuthStore } from './auth'
5
6export const useMissionsStore = defineStore('missions', () => {
7  const missions = ref([])
8
9  async function fetchMissions() {
10    // Użyj auth store do pobrania tokena
11    const authStore = useAuthStore()
12
13    if (!authStore.isAuthenticated) {
14      throw new Error('Must be logged in')
15    }
16
17    const res = await fetch('/api/missions', {
18      headers: {
19        'Authorization': `Bearer ${authStore.token}`
20      }
21    })
22
23    missions.value = await res.json()
24  }
25
26  return { missions, fetchMissions }
27})

Wzajemne zależności stores

Stores mogą nawzajem się używać, ale wywołuj useStore() wewnątrz actions (nie na górze setup function), aby uniknąć problemów z cyklicznymi zależnościami:

1// stores/notifications.js
2import { defineStore } from 'pinia'
3import { ref } from 'vue'
4
5export const useNotificationStore = defineStore('notifications', () => {
6  const notifications = ref([])
7
8  function add(message, type = 'info') {
9    notifications.value.push({
10      id: Date.now(),
11      message,
12      type,
13      timestamp: new Date()
14    })
15  }
16
17  function remove(id) {
18    notifications.value = notifications.value.filter(n => n.id !== id)
19  }
20
21  return { notifications, add, remove }
22})
23
24// stores/resources.js - używa notifications
25import { useNotificationStore } from './notifications'
26
27export const useResourceStore = defineStore('resources', () => {
28  const oxygen = ref(100)
29
30  function consumeOxygen(amount) {
31    oxygen.value -= amount
32
33    if (oxygen.value < 20) {
34      // Wywołaj useStore WEWNĄTRZ action
35      const notifStore = useNotificationStore()
36      notifStore.add('Oxygen level critical!', 'danger')
37    }
38  }
39
40  return { oxygen, consumeOxygen }
41})

Architektura wielu stores

Zaleca się tworzenie wielu małych, specjalizowanych stores zamiast jednego dużego:

1stores/
2├── auth.js         # Autoryzacja użytkownika
3├── resources.js    # Zasoby stacji (tlen, woda, energia)
4├── crew.js         # Dane załogi
5├── missions.js     # Misje i zadania
6├── notifications.js # System powiadomień
7└── settings.js     # Ustawienia aplikacji

Każdy store odpowiada za jeden "moduł" aplikacji - tak jak każdy moduł stacji NOVA LAB ma swoją funkcję.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// Kompozycja stores - AuthStore
5const user = ref({ name: 'Komandor Nova', role: 'admin' })
6const token = ref('jwt-token-xyz')
7const isAuthenticated = computed(() => !!token.value)
8
9// Kompozycja stores - ResourceStore (korzysta z AuthStore)
10const resources = ref([
11  { id: 1, name: 'Tlen', level: 92, unit: '%' },
12  { id: 2, name: 'Woda', level: 78, unit: '%' },
13  { id: 3, name: 'Energia', level: 65, unit: '%' }
14])
15
16// Kompozycja stores - NotificationStore
17const notifications = ref([])
18function addNotification(message, type = 'info') {
19  const id = Date.now()
20  notifications.value.push({ id, message, type, time: new Date().toLocaleTimeString() })
21  setTimeout(() => {
22    notifications.value = notifications.value.filter(n => n.id !== id)
23  }, 4000)
24}
25
26// Actions z ResourceStore korzystają z NotificationStore
27function consumeResource(id, amount) {
28  const resource = resources.value.find(r => r.id === id)
29  if (!resource) return
30
31  resource.level = Math.max(0, resource.level - amount)
32
33  if (resource.level < 20) {
34    // Między stores: zasoby -> powiadomienia
35    addNotification(`${resource.name} - krytycznie niski poziom: ${resource.level}${resource.unit}!`, 'danger')
36  } else if (resource.level < 50) {
37    addNotification(`${resource.name} poniżej 50%: ${resource.level}${resource.unit}`, 'warning')
38  }
39}
40
41function refillResource(id) {
42  const resource = resources.value.find(r => r.id === id)
43  if (resource) {
44    resource.level = 100
45    addNotification(`${resource.name}: uzupełniono do 100%`, 'success')
46  }
47}
48
49// Getter łączący stores
50const criticalResources = computed(() =>
51  resources.value.filter(r => r.level < 30)
52)
53</script>
54
55<template>
56  <div class="nova-lab">
57    <h1>NOVA LAB - kompozycja stores</h1>
58
59    <div class="user-bar" v-if="isAuthenticated">
60      Zalogowano: {{ user.name }} ({{ user.role }})
61    </div>
62
63    <div class="resources-grid">
64      <div v-for="r in resources" :key="r.id"
65        class="resource-card" :class="{ critical: r.level < 20, warning: r.level < 50 }">
66        <h3>{{ r.name }}</h3>
67        <div class="level-bar">
68          <div class="level-fill" :style="{ width: r.level + '%' }"></div>
69        </div>
70        <div class="level-text">{{ r.level }}{{ r.unit }}</div>
71        <div class="r-actions">
72          <button @click="consumeResource(r.id, 15)" class="btn">-15</button>
73          <button @click="refillResource(r.id)" class="btn btn-green">Uzupełnij</button>
74        </div>
75      </div>
76    </div>
77
78    <div v-if="criticalResources.length" class="critical-alert">
79      KRYTYCZNE: {{ criticalResources.map(r => r.name).join(', ') }}
80    </div>
81
82    <div class="notifications">
83      <div v-for="n in notifications" :key="n.id" class="notif" :class="n.type">
84        {{ n.time }} - {{ n.message }}
85      </div>
86    </div>
87  </div>
88</template>
89
90<style scoped>
91.nova-lab {
92  background: #0a0e27;
93  color: #00ff88;
94  min-height: 100vh;
95  padding: 20px;
96  font-family: monospace;
97}
98h1 { color: #00ff88; margin-bottom: 15px; text-align: center; }
99.user-bar {
100  text-align: center;
101  padding: 8px;
102  background: rgba(0, 180, 216, 0.1);
103  border: 1px solid #00b4d8;
104  margin-bottom: 15px;
105  color: #00b4d8;
106}
107.resources-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 15px; }
108.resource-card {
109  background: rgba(0, 255, 136, 0.05);
110  border: 2px solid #00ff88;
111  padding: 15px;
112  text-align: center;
113}
114.resource-card.warning { border-color: #ff9900; }
115.resource-card.critical { border-color: #ff4444; background: rgba(255, 68, 68, 0.1); }
116h3 { color: #00b4d8; margin-bottom: 8px; }
117.level-bar { height: 12px; background: #1a1e3f; border: 1px solid #00b4d8; margin: 8px 0; }
118.level-fill { height: 100%; background: #00ff88; transition: width 0.3s; }
119.resource-card.warning .level-fill { background: #ff9900; }
120.resource-card.critical .level-fill { background: #ff4444; }
121.level-text { font-size: 1.2em; font-weight: bold; margin-bottom: 8px; }
122.r-actions { display: flex; gap: 5px; justify-content: center; }
123.btn {
124  background: #00b4d8;
125  color: #0a0e27;
126  border: none;
127  padding: 5px 12px;
128  cursor: pointer;
129  font-family: monospace;
130}
131.btn-green { background: #00ff88; }
132.critical-alert {
133  padding: 10px;
134  background: rgba(255, 68, 68, 0.2);
135  border: 2px solid #ff4444;
136  color: #ff4444;
137  text-align: center;
138  margin-bottom: 15px;
139  font-weight: bold;
140}
141.notifications { max-width: 500px; margin: 0 auto; }
142.notif {
143  padding: 8px;
144  margin: 4px 0;
145  border: 1px solid;
146  font-size: 0.9em;
147}
148.notif.info { border-color: #00b4d8; color: #00b4d8; }
149.notif.warning { border-color: #ff9900; color: #ff9900; }
150.notif.danger { border-color: #ff4444; color: #ff4444; }
151.notif.success { border-color: #00ff88; color: #00ff88; }
152</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. Czy store w Pinia może importować i używać innego store?

  2. 2. Gdzie najlepiej wywoływać useStore() drugiego store wewnątrz store?

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

Zadania praktyczne w grze

  • Edytor kodu

    Stwórz tasks store używający auth store - fetchTasks sprawdza isAuthenticated.

  • Układanie w pionie

    Uporządkuj stores od najbardziej niezależnego do najbardziej zależnego:

  • Klikanie w kolejności

    Ułóż kod action wywołującej inny store:

  • Edytor kodu

    Stwórz resource store z alertami - consume generuje alerty przy niskim poziomie.

Przydatne artykuły