Kurs Vue.js · Moduł 11: Pinia
Composition API Stores (setup syntax)
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?
| Aspekt | Composition API | Options API |
|---|---|---|
| Elastyczność | Pełna | Ograniczona |
| TypeScript | Naturalne typowanie | Wymaga deklaracji |
$reset() | Trzeba zdefiniować ręcznie | Działa automatycznie |
| Composables | Łatwe do użycia | Trudniejsze |
| Składnia | Jak <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. Jaki styl definiowania store jest zalecany w Pinia?
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.