Kurs Vue.js · Moduł 3: Reaktywność
Reaktywność w Vue - Żywy Reaktor Plazmowy
W tej lekcji5
Witaj w Reaktorze Plazmowym NOVA LAB! Dr. Nova mówi: tu czujniki ożywają - tak jak dane w Vue stają się reaktywne i automatycznie aktualizują interfejs kontrolny. Każdy odczyt mocy, temperatury czy ciśnienia jest natychmiast widoczny na panelach.
W module 2 pisałeś szablony, które same odświeżały się po zmianie danych, i przyjmowałeś to na wiarę. W tym module zaglądamy do środka tego mechanizmu, bo bez zrozumienia go prędzej czy później trafisz na panel, który uparcie pokazuje stary odczyt.
Czego się nauczysz
- wyjaśniać, czym jest reaktywność i jak Vue śledzi zmiany danych,
- tworzyć reaktywne wartości przez
ref()i świadomie używać.value, - budować reaktywne obiekty przez
reactive()i omijać jego ograniczenia, - dobierać głęboką lub płytką reaktywność:
shallowRef,shallowReactive,triggerRef, - bezpiecznie rozpakowywać obiekty dzięki
toRefitoRefs, - diagnozować stan narzędziami
isRef,isReactive,unref,toRawimarkRaw, - chronić stan przez
readonly()i układać go w czytelną architekturę.
Czym jest Reaktywność?
Zacznijmy od problemu. W czystym JavaScripcie zmiana zmiennej nie zostawia żadnego śladu, więc ekran nie ma skąd się o niej dowiedzieć:
1let powerLevel = 0
2document.querySelector('#power').textContent = powerLevel
3
4powerLevel = 75
5// ekran nadal pokazuje 0 - nikt nie wie o zmianieAby odświeżyć panel, musiałbyś po każdej zmianie ręcznie odszukać element i wpisać nową wartość. Reaktywność to system, który automatycznie śledzi zmiany danych i aktualizuje DOM. Kiedy zmieniasz wartość - jak poziom mocy reaktora - Vue "wie" i odświeża widok na panelu kontrolnym.
1<template>
2 <div class="reactor-panel" style="background: #0a0e27; color: #00ff88;">
3 <p>Status: {{ reactorStatus }}</p>
4 <button @click="changeStatus">Zmień status</button>
5 </div>
6</template>
7
8<script setup>
9import { ref } from 'vue'
10
11const reactorStatus = ref('STANDBY')
12
13function changeStatus() {
14 reactorStatus.value = 'ACTIVE'
15 // Vue automatycznie zaktualizuje panel kontrolny!
16}
17</script>Podczas renderowania szablon odczytuje reactorStatus, a Vue zapisuje: ten komponent zależy od tego czujnika. Gdy changeStatus przypisze nową wartość, Vue ponownie renderuje zależny komponent. Aktualizacje DOM są grupowane i wykonywane w najbliższym takcie (nextTick), więc dziesięć zmian pod rząd daje jedno odświeżenie.
ref() - Reaktywny Czujnik
ref() tworzy reaktywne "opakowanie" dla wartości - jak czujnik reaktora monitorujący parametry. Przyjmuje wartość początkową dowolnego typu i zwraca kontener z właściwością .value:
1import { ref } from 'vue'
2
3// Dla prymitywów - pojedyncze odczyty
4const powerLevel = ref(0)
5const statusMessage = ref('System initializing...')
6const isOnline = ref(true)
7
8// Dla obiektów (też działa) - kompleksowe dane sensora
9const reactor = ref({
10 name: 'Plasma Core Alpha',
11 temperature: 2500
12})
13
14// Dostęp do wartości - używaj .value
15console.log(powerLevel.value) // 0
16powerLevel.value++
17console.log(powerLevel.value) // 1
18
19// W template - bez .value!
20// {{ powerLevel }} - Vue robi to automatycznieW skrypcie zawsze piszesz .value. Refy zadeklarowane na najwyższym poziomie <script setup> szablon rozpakowuje sam, więc tam .value jest zbędne. Zmienna pozostaje const: zmienia się zawartość kontenera, nie sam kontener. W module 2 taki kontener przechowywał też element DOM - to wciąż ta sama funkcja, a element wkładał do niej atrybut ref.
Dlaczego .value?
JavaScript nie potrafi przechwycić odczytu ani zapisu zwykłej zmiennej, ale potrafi przechwycić dostęp do właściwości obiektu. ref() opakowuje wartość w obiekt z właściwością .value - jak czujnik pakujący surowe dane do transmisji:
1const powerLevel = ref(0)
2
3// Pod spodem:
4// powerLevel = { value: 0 }
5
6// Zmiana wartości
7powerLevel.value = 75 // setter .value wykrywa zmianę → aktualizacja paneluZapis { value: 0 } to uproszczenie. Naprawdę .value ma getter, który zapamiętuje, kto czyta odczyt, i setter, który powiadamia tych czytelników o zmianie. Obiekty z reactive() z następnej lekcji działają inaczej: tam Vue używa mechanizmu Proxy.
Typowanie ref (TypeScript)
W komponencie z <script setup lang="ts"> TypeScript zwykle sam wywnioskuje typ refa. Typ Ref<T> importujesz przez import type, gdy chcesz go zapisać jawnie, a interfejs Sensor opisuje kształt danych czujnika:
1import { ref } from 'vue'
2import type { Ref } from 'vue'
3
4// Automatyczne wnioskowanie typu
5const temperature = ref(0) // Ref<number>
6const sensorName = ref('Temperature Alpha') // Ref<string>
7
8// Jawne typowanie
9const missionYear: Ref<number> = ref(2087)
10
11// Dla złożonych typów
12interface Sensor {
13 name: string
14 unit: string
15 maxValue: number
16}
17
18const temperatureSensor = ref<Sensor>({
19 name: 'Plasma Core Temperature',
20 unit: 'Kelvin',
21 maxValue: 5000
22})Zapis ref<Sensor>() podaje typ w nawiasach ostrych. Gdy podasz typ, ale pominiesz wartość początkową, typ obejmie też undefined.
Dokumentacja Vue zaleca ref() jako podstawowe narzędzie do deklarowania stanu i ja też tak pracuję. W następnej lekcji zobaczysz reactive(), który obejmuje cały panel naraz, ale ma swoje ograniczenia.
Zapamiętaj: ref() to czujnik w kapsule - w skrypcie otwierasz kapsułę przez .value, a hologram szablonu robi to za Ciebie.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, reactive } from 'vue'
3
4// Reaktor Plazmowy - Podstawowa reaktywność
5// ref() dla wartości prymitywnych (liczby, stringi, boolean)
6const reactorPower = ref(0)
7const isOnline = ref(false)
8const reactorStatus = ref('OFFLINE')
9
10// reactive() dla obiektów
11const plasmaCore = reactive({
12 temperature: 0,
13 pressure: 0,
14 stability: 100,
15 fuelLevel: 100
16})
17
18function startReactor() {
19 isOnline.value = true
20 reactorStatus.value = 'INITIALIZING'
21
22 // Symulacja uruchamiania reaktora
23 const interval = setInterval(() => {
24 if (reactorPower.value < 100) {
25 reactorPower.value += 10
26 plasmaCore.temperature += 150
27 plasmaCore.pressure += 5
28 plasmaCore.fuelLevel -= 2
29 } else {
30 reactorStatus.value = 'ONLINE'
31 clearInterval(interval)
32 }
33 }, 300)
34}
35
36function shutdownReactor() {
37 isOnline.value = false
38 reactorStatus.value = 'OFFLINE'
39 reactorPower.value = 0
40 plasmaCore.temperature = 0
41 plasmaCore.pressure = 0
42 plasmaCore.stability = 100
43 plasmaCore.fuelLevel = 100
44}
45</script>
46
47<template>
48 <div class="reactor-control">
49 <div class="header">
50 <h1>REAKTOR PLAZMOWY - NOVA LAB</h1>
51 <p class="subtitle">System Reaktywności Vue.js - ref() i reactive()</p>
52 </div>
53
54 <div class="status-grid">
55 <div class="status-card">
56 <div class="label">STATUS REAKTORA</div>
57 <div :class="['value', isOnline ? 'online' : 'offline']">
58 {{ reactorStatus }}
59 </div>
60 </div>
61
62 <div class="status-card">
63 <div class="label">MOC WYJŚCIOWA</div>
64 <div class="value accent">{{ reactorPower }}%</div>
65 <div class="progress-bar">
66 <div class="progress-fill" :style="{ width: reactorPower + '%' }"></div>
67 </div>
68 </div>
69 </div>
70
71 <div class="plasma-grid">
72 <div class="plasma-card">
73 <div class="icon">◆</div>
74 <div class="label">TEMPERATURA</div>
75 <div class="value">{{ plasmaCore.temperature.toFixed(0) }}°C</div>
76 </div>
77
78 <div class="plasma-card">
79 <div class="icon">◆</div>
80 <div class="label">CIŚNIENIE</div>
81 <div class="value">{{ plasmaCore.pressure }} bar</div>
82 </div>
83
84 <div class="plasma-card">
85 <div class="icon">◆</div>
86 <div class="label">STABILNOŚĆ</div>
87 <div class="value">{{ plasmaCore.stability }}%</div>
88 </div>
89
90 <div class="plasma-card">
91 <div class="icon">◆</div>
92 <div class="label">PALIWO</div>
93 <div class="value">{{ plasmaCore.fuelLevel }}%</div>
94 </div>
95 </div>
96
97 <div class="controls">
98 <button
99 @click="startReactor"
100 :disabled="isOnline"
101 class="btn btn-start"
102 >
103 URUCHOM REAKTOR
104 </button>
105 <button
106 @click="shutdownReactor"
107 :disabled="!isOnline"
108 class="btn btn-stop"
109 >
110 WYŁĄCZ REAKTOR
111 </button>
112 </div>
113
114 <div class="info-box">
115 <h3>Nauka Reaktywności</h3>
116 <p><strong>ref():</strong> Używamy dla wartości prymitywnych (reactorPower, isOnline)</p>
117 <p><strong>reactive():</strong> Używamy dla obiektów (plasmaCore)</p>
118 <p><strong>Dostęp:</strong> ref wymaga .value, reactive nie wymaga</p>
119 </div>
120 </div>
121</template>
122
123<style scoped>
124.reactor-control {
125 min-height: 100vh;
126 background: #0a0e27;
127 color: #e0e0e0;
128 padding: 2rem;
129 font-family: 'Courier New', monospace;
130}
131
132.header {
133 text-align: center;
134 margin-bottom: 2rem;
135 border-bottom: 2px solid #00ff88;
136 padding-bottom: 1rem;
137}
138
139.header h1 {
140 color: #00ff88;
141 font-size: 2rem;
142 margin: 0;
143 text-shadow: 0 0 10px #00ff88;
144}
145
146.subtitle {
147 color: #00b4d8;
148 margin: 0.5rem 0 0 0;
149}
150
151.status-grid {
152 display: grid;
153 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
154 gap: 1rem;
155 margin-bottom: 2rem;
156}
157
158.status-card {
159 background: #1a1f3a;
160 padding: 1.5rem;
161 border-radius: 8px;
162 border: 1px solid #00b4d8;
163}
164
165.label {
166 font-size: 0.9rem;
167 color: #888;
168 margin-bottom: 0.5rem;
169}
170
171.value {
172 font-size: 1.8rem;
173 font-weight: bold;
174 color: #fff;
175}
176
177.value.online {
178 color: #00ff88;
179 text-shadow: 0 0 10px #00ff88;
180}
181
182.value.offline {
183 color: #ff4444;
184}
185
186.value.accent {
187 color: #00b4d8;
188}
189
190.progress-bar {
191 height: 8px;
192 background: #0a0e27;
193 border-radius: 4px;
194 margin-top: 0.5rem;
195 overflow: hidden;
196}
197
198.progress-fill {
199 height: 100%;
200 background: linear-gradient(90deg, #00b4d8, #00ff88);
201 transition: width 0.3s ease;
202}
203
204.plasma-grid {
205 display: grid;
206 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
207 gap: 1rem;
208 margin-bottom: 2rem;
209}
210
211.plasma-card {
212 background: #1a1f3a;
213 padding: 1.5rem;
214 border-radius: 8px;
215 border: 1px solid #333;
216 text-align: center;
217}
218
219.plasma-card .icon {
220 font-size: 2rem;
221 margin-bottom: 0.5rem;
222}
223
224.plasma-card .value {
225 font-size: 1.5rem;
226 color: #00ff88;
227}
228
229.controls {
230 display: flex;
231 gap: 1rem;
232 margin-bottom: 2rem;
233 justify-content: center;
234}
235
236.btn {
237 padding: 1rem 2rem;
238 font-size: 1.1rem;
239 border: none;
240 border-radius: 8px;
241 cursor: pointer;
242 font-weight: bold;
243 transition: all 0.3s ease;
244 font-family: 'Courier New', monospace;
245}
246
247.btn-start {
248 background: linear-gradient(135deg, #00ff88, #00b4d8);
249 color: #0a0e27;
250}
251
252.btn-start:hover:not(:disabled) {
253 transform: scale(1.05);
254 box-shadow: 0 0 20px #00ff88;
255}
256
257.btn-stop {
258 background: linear-gradient(135deg, #ff4444, #ff6666);
259 color: white;
260}
261
262.btn-stop:hover:not(:disabled) {
263 transform: scale(1.05);
264 box-shadow: 0 0 20px #ff4444;
265}
266
267.btn:disabled {
268 opacity: 0.5;
269 cursor: not-allowed;
270}
271
272.info-box {
273 background: #1a1f3a;
274 padding: 1.5rem;
275 border-radius: 8px;
276 border-left: 4px solid #00ff88;
277}
278
279.info-box h3 {
280 color: #00ff88;
281 margin-top: 0;
282}
283
284.info-box p {
285 margin: 0.5rem 0;
286 color: #ccc;
287}
288
289.info-box strong {
290 color: #00b4d8;
291}
292</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. Czym jest reaktywność w Vue.js?
2. Do czego służy ref() w Vue 3?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Utwórz reaktywną zmienną z ref() i wyświetl ją w template.
- Klikanie w kolejności
Ułóż kroki użycia ref() w Vue:
- Edytor kodu
Stwórz licznik z przyciskami do zwiększania i zmniejszania wartości ref().
- Układanie w pionie
Ułóż proces tworzenia i użycia reaktywnego stanu z ref():