Kurs Vue.js · Moduł 3: Reaktywność

Reaktywność w Vue - Żywy Reaktor Plazmowy

4 min czytania
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 toRef i toRefs,
  • diagnozować stan narzędziami isRef, isReactive, unref, toRaw i markRaw,
  • 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 zmianie

Aby 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 automatycznie

W 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 panelu

Zapis { 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. 1. Czym jest reaktywność w Vue.js?

  2. 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():

Przydatne artykuły