Kurs Vue.js · Moduł 11: Pinia

Composition API Stores (setup syntax)

3 min czytania
W tej lekcji5

W NOVA LAB każdy system ma swój protokół konfiguracji. Pinia oferuje dwa style definiowania stores - Composition API (setup function) i Options API. Zalecany jest Composition API, bo jest bliższy temu jak piszemy komponenty Vue 3.

Composition API vs Options API

1// Composition API (ZALECANY)
2export const useCounterStore = defineStore('counter', () => {
3  const count = ref(0)
4  const doubleCount = computed(() => count.value * 2)
5  function increment() { count.value++ }
6
7  return { count, doubleCount, increment }
8})
9
10// Options API
11export const useCounterStore = defineStore('counter', {
12  state: () => ({ count: 0 }),
13  getters: {
14    doubleCount: (state) => state.count * 2
15  },
16  actions: {
17    increment() { this.count++ }
18  }
19})

Kiedy wybrać który styl?

AspektComposition APIOptions API
ElastycznośćPełnaOgraniczona
TypeScriptNaturalne typowanieWymaga deklaracji
$reset()Trzeba zdefiniować ręcznieDziała automatycznie
ComposablesŁatwe do użyciaTrudniejsze
SkładniaJak <script setup>Jak Options API Vue

Użycie composables w store

Composition API pozwala łatwo używać composables wewnątrz store:

1import { defineStore } from 'pinia'
2import { ref, computed, watch } from 'vue'
3import { useLocalStorage } from '@vueuse/core'
4
5export const useSettingsStore = defineStore('settings', () => {
6  // Composable w store!
7  const theme = useLocalStorage('theme', 'dark')
8  const language = useLocalStorage('language', 'pl')
9
10  const isDark = computed(() => theme.value === 'dark')
11
12  function toggleTheme() {
13    theme.value = isDark.value ? 'light' : 'dark'
14  }
15
16  return { theme, language, isDark, toggleTheme }
17})

Watchers w store

Możesz używać watch i watchEffect wewnątrz store:

1export const useAlertStore = defineStore('alerts', () => {
2  const temperature = ref(22)
3  const alerts = ref([])
4
5  // Watcher reagujący na zmiany
6  watch(temperature, (newTemp, oldTemp) => {
7    if (newTemp > 50) {
8      alerts.value.push({
9        type: 'critical',
10        message: `Temperature critical: ${newTemp}°C`,
11        timestamp: Date.now()
12      })
13    }
14  })
15
16  function setTemperature(value) {
17    temperature.value = value
18  }
19
20  return { temperature, alerts, setTemperature }
21})

Typowanie z TypeScript

1interface CrewMember {
2  id: number
3  name: string
4  role: 'commander' | 'engineer' | 'scientist'
5  active: boolean
6}
7
8export const useCrewStore = defineStore('crew', () => {
9  const members = ref<CrewMember[]>([])
10  const selected = ref<CrewMember | null>(null)
11
12  const activeMembers = computed<CrewMember[]>(() =>
13    members.value.filter(m => m.active)
14  )
15
16  function addMember(member: Omit<CrewMember, 'id'>): CrewMember {
17    const newMember: CrewMember = {
18      ...member,
19      id: Date.now()
20    }
21    members.value.push(newMember)
22    return newMember
23  }
24
25  return { members, selected, activeMembers, addMember }
26})
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed, watch } from 'vue'
3
4// Store w stylu Composition API z watcherami i wzorcami z TypeScript
5const temperature = ref(22)
6const alerts = ref([])
7const settings = ref({
8  theme: 'dark',
9  language: 'pl',
10  unit: 'celsius'
11})
12
13// Watcher wewnątrz store
14watch(temperature, (newTemp) => {
15  if (newTemp > 45) {
16    alerts.value.push({
17      id: Date.now(),
18      type: 'critical',
19      message: `Temperatura krytyczna: ${newTemp} C`,
20      time: new Date().toLocaleTimeString()
21    })
22  } else if (newTemp > 35) {
23    alerts.value.push({
24      id: Date.now(),
25      type: 'warning',
26      message: `Wysoka temperatura: ${newTemp} C`,
27      time: new Date().toLocaleTimeString()
28    })
29  }
30})
31
32// Getters
33const isDark = computed(() => settings.value.theme === 'dark')
34const tempDisplay = computed(() => {
35  if (settings.value.unit === 'fahrenheit') {
36    return ((temperature.value * 9/5) + 32).toFixed(1) + ' F'
37  }
38  return temperature.value + ' C'
39})
40
41const criticalAlerts = computed(() =>
42  alerts.value.filter(a => a.type === 'critical')
43)
44
45// Actions
46function setTemperature(value) {
47  temperature.value = Math.max(-100, Math.min(100, value))
48}
49
50function toggleTheme() {
51  settings.value.theme = isDark.value ? 'light' : 'dark'
52}
53
54function toggleUnit() {
55  settings.value.unit = settings.value.unit === 'celsius' ? 'fahrenheit' : 'celsius'
56}
57
58function dismissAlert(id) {
59  alerts.value = alerts.value.filter(a => a.id !== id)
60}
61
62function clearAlerts() {
63  alerts.value = []
64}
65</script>
66
67<template>
68  <div class="nova-lab" :class="settings.theme">
69    <h1>NOVA LAB - store w Composition API</h1>
70
71    <div class="temp-control">
72      <h2>Sterowanie temperaturą</h2>
73      <div class="temp-display" :class="{ critical: temperature > 45, warning: temperature > 35 }">
74        {{ tempDisplay }}
75      </div>
76      <div class="slider-row">
77        <button @click="setTemperature(temperature - 5)" class="btn">-5</button>
78        <input type="range" v-model.number="temperature" min="-50" max="80" class="slider" />
79        <button @click="setTemperature(temperature + 5)" class="btn">+5</button>
80      </div>
81    </div>
82
83    <div class="settings-panel">
84      <h2>Ustawienia (wzorzec composable)</h2>
85      <button @click="toggleTheme" class="btn">
86        Motyw: {{ settings.theme }}
87      </button>
88      <button @click="toggleUnit" class="btn">
89        Jednostka: {{ settings.unit }}
90      </button>
91    </div>
92
93    <div v-if="alerts.length" class="alerts-panel">
94      <div class="alerts-header">
95        <h2>Alarmy ({{ alerts.length }})</h2>
96        <button @click="clearAlerts" class="btn btn-sm">Wyczyść wszystko</button>
97      </div>
98      <div v-for="alert in alerts" :key="alert.id"
99        class="alert-item" :class="alert.type">
100        <span>{{ alert.time }} - {{ alert.message }}</span>
101        <button @click="dismissAlert(alert.id)" class="dismiss">x</button>
102      </div>
103    </div>
104  </div>
105</template>
106
107<style scoped>
108.nova-lab {
109  min-height: 100vh;
110  padding: 20px;
111  font-family: monospace;
112  transition: all 0.3s;
113}
114.nova-lab.dark { background: #0a0e27; color: #00ff88; }
115.nova-lab.light { background: #f0f4f8; color: #1a1e3f; }
116h1 { margin-bottom: 20px; text-align: center; }
117h2 { color: #00b4d8; margin-bottom: 10px; }
118.temp-control, .settings-panel, .alerts-panel {
119  max-width: 500px;
120  margin: 0 auto 15px;
121  padding: 15px;
122  border: 2px solid #00b4d8;
123  background: rgba(0, 180, 216, 0.1);
124}
125.temp-display {
126  text-align: center;
127  font-size: 2em;
128  font-weight: bold;
129  margin: 10px 0;
130}
131.temp-display.warning { color: #ff9900; }
132.temp-display.critical { color: #ff4444; }
133.slider-row { display: flex; align-items: center; gap: 10px; }
134.slider { flex: 1; }
135.btn {
136  background: #00b4d8;
137  color: #0a0e27;
138  border: none;
139  padding: 6px 14px;
140  cursor: pointer;
141  font-family: monospace;
142  margin: 2px;
143}
144.btn:hover { background: #00ff88; }
145.btn-sm { padding: 4px 10px; font-size: 0.85em; }
146.alerts-header { display: flex; justify-content: space-between; align-items: center; }
147.alert-item {
148  display: flex;
149  justify-content: space-between;
150  align-items: center;
151  padding: 8px;
152  margin: 5px 0;
153  border: 1px solid;
154}
155.alert-item.warning { border-color: #ff9900; color: #ff9900; }
156.alert-item.critical { border-color: #ff4444; color: #ff4444; }
157.dismiss {
158  background: none;
159  border: 1px solid currentColor;
160  color: inherit;
161  cursor: pointer;
162  padding: 2px 6px;
163  font-family: monospace;
164}
165</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. Jaki styl definiowania store jest zalecany w Pinia?

  2. 2. Czy można używać watch() wewnątrz store (Composition API)?

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

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj elementy store od definiowania do eksportowania:

  • Edytor kodu

    Zdefiniuj settings store z theme, language, getter isDark i toggleTheme action.

  • Układanie w poziomie

    Ułóż typowaną definicję state w store:

  • Edytor kodu

    Zdefiniuj crew store z getterami, actions i custom $reset.

Przydatne artykuły