Kurs Vue.js · Moduł 3: Reaktywność
reactive() - Reaktywne Systemy
W tej lekcji4
Gdy pracujesz z kompleksowymi obiektami systemów, reactive() jest często wygodniejszy niż ref() - to jak kompletny panel kontrolny reaktora zamiast pojedynczych wskaźników.
Reaktor ma nazwę, status, temperaturę i dwa odczyty: moc i promieniowanie. Pięć osobnych refów oznacza pięć deklaracji i .value w każdej linii skryptu, choć wszystkie te dane opisują jedno urządzenie. reactive() pozwala zamknąć je w jednym obiekcie, który cały jest reaktywny.
Podstawy reactive()
reactive() przyjmuje obiekt i zwraca jego Proxy - wbudowany w JavaScript mechanizm, który przechwytuje każdy odczyt i zapis właściwości. Dzięki temu nie potrzebujesz .value, a zagnieżdżone obiekty też stają się reaktywne:
1import { reactive } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 status: 'ACTIVE',
6 temperature: 2500,
7 readings: {
8 power: 850,
9 radiation: 12
10 }
11})
12
13// Dostęp bez .value!
14console.log(reactor.name) // 'Plasma Core Alpha'
15reactor.temperature = 2600
16
17// Zagnieżdżone obiekty też są reaktywne
18reactor.readings.power = 900 // To też wywoła aktualizację paneluProxy działa jak dyspozytor między Tobą a danymi: każdą zmianę zgłasza systemowi, który odświeża zależne widoki. Zauważ, co się nie zmieniło - reaktywny jest tylko obiekt zwrócony przez reactive(), a nie oryginał, który do niego podałeś:
1const raw = { status: 'STANDBY' }
2const panel = reactive(raw)
3
4console.log(panel === raw) // false
5console.log(reactive(raw) === panel) // trueProxy to inny obiekt niż oryginał, a zmiany zapisane wprost w raw nie odświeżą widoku. Ponowne wywołanie reactive() dla tego samego obiektu zwraca to samo Proxy, więc zawsze pracuj na wersji reaktywnej.
reactive() vs ref()
Oba narzędzia dają reaktywność, ale różnią się w codziennym użyciu:
| Cecha | ref() | reactive() |
|---|---|---|
| Dla prymitywów | Tak | Nie |
| Dla obiektów | Tak (.value) | Tak (bezpośrednio) |
| Destructuring | Bezpieczne (przekazujesz cały kontener) | Traci reaktywność |
| Reassign | Tak (.value = newObj) | Nie (tylko właściwości) |
Ref możesz przekazać do funkcji albo wyjąć z obiektu i nadal będzie reaktywny, bo krąży cały kontener. Z reactive() wyciągasz natomiast gołe wartości. Dokumentacja Vue zaleca ref() jako podstawowe API właśnie z powodu tych ograniczeń.
Praktyczny Przykład
Oto panel reaktora zbudowany na jednym obiekcie. Szablon czyta także zagnieżdżone odczyty:
1<template>
2 <div class="reactor-panel" style="background: #0a0e27; color: #00ff88; border: 1px solid #00b4d8;">
3 <h2>{{ reactor.name }}</h2>
4 <p>Status: {{ reactor.status }}</p>
5 <p>Temperatura: {{ reactor.temperature }} K</p>
6 <p>Odczyty: {{ reactor.readings.power }} MW | {{ reactor.readings.radiation }} Sv/h</p>
7
8 <button @click="updateTemperature">Zwiększ temperaturę</button>
9 </div>
10</template>W szablonie nie ma ani jednego .value, bo reactor jest obiektem reaktywnym, a nie refem. Skrypt tworzy ten obiekt i funkcję, która zmienia jedną właściwość:
1<script setup>
2import { reactive } from 'vue'
3
4const reactor = reactive({
5 name: 'Plasma Core Alpha',
6 status: 'ACTIVE',
7 temperature: 2500,
8 readings: {
9 power: 850,
10 radiation: 12
11 }
12})
13
14function updateTemperature() {
15 reactor.temperature += 100 // Bezpośrednia modyfikacja
16}
17</script>Każde kliknięcie podnosi temperaturę o 100 K, a panel pokazuje nową wartość. Zmienna reactor przez cały czas wskazuje na to samo Proxy - zmieniają się tylko jego właściwości.
Ograniczenia reactive()
Pierwsze ograniczenie: reactive() działa tylko z obiektami, tablicami oraz kolekcjami Map i Set. Dla liczby Vue wypisze ostrzeżenie i zwróci ją bez zmian. Drugie dotyczy podmiany całego obiektu:
1import { reactive } from 'vue'
2
3const reactor = reactive({ name: 'Plasma Core Alpha' })
4
5// ŹLE - zastąpienie całego obiektu
6reactor = { name: 'Plasma Core Beta' } // To nie zadziała!
7
8// DOBRZE - modyfikacja właściwości
9reactor.name = 'Plasma Core Beta'Przy const sam JavaScript rzuci TypeError (przypisanie do stałej). Przy let zmienna wskazałaby nowy, zwykły obiekt, a szablon zostałby przy starym Proxy, więc połączenie by się zerwało. Dlatego zmieniasz właściwości, a nie cały obiekt. Gdy nowe dane przychodzą w całości, na przykład z telemetrii, przepisz je przez Object.assign(reactor, newData) - to nadal zmiana właściwości tego samego Proxy. Trzecie ograniczenie to destrukturyzacja:
1// ŹLE - destructuring traci reaktywność
2const { name } = reactor // name nie jest reaktywny!
3
4// DOBRZE - użyj toRefs
5import { toRefs } from 'vue'
6const { name } = toRefs(reactor) // name.value jest reaktywnyDestrukturyzacja kopiuje bieżącą wartość, a toRefs tworzy refy powiązane z właściwościami - dokładnie omówimy go w lekcji o toRef i toRefs. Moja praktyka w NOVA LAB: domyślnie ref(), a reactive() dla spójnej grupy pól, na przykład formularza konfiguracji. W następnej lekcji sprawdzisz, jak głęboko sięga ta reaktywność i kiedy warto ją spłycić.
Zapamiętaj: reactive() to panel kontrolny za szybą Proxy - kręcisz pokrętłami, ale nigdy nie wymieniasz całego panelu.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, reactive } from 'vue'
3
4// Porównanie ref() vs reactive()
5// Zarządzanie zbiornikami tlenu na stacji NOVA LAB
6
7// ref() - pojedyncze wartości
8const mainTankLevel = ref(100)
9const backupTankLevel = ref(75)
10const oxygenFlow = ref(2.5)
11
12// reactive() - kompleksowy obiekt
13const oxygenSystem = reactive({
14 mainTank: {
15 level: 100,
16 capacity: 1000,
17 pressure: 150,
18 status: 'OPTIMAL'
19 },
20 backupTank: {
21 level: 75,
22 capacity: 500,
23 pressure: 120,
24 status: 'STANDBY'
25 },
26 flow: {
27 rate: 2.5,
28 direction: 'OUT',
29 consumers: 12
30 }
31})
32
33// Symulacja zużycia tlenu
34let simulationInterval = null
35
36function startSimulation() {
37 if (simulationInterval) return
38
39 simulationInterval = setInterval(() => {
40 // Używanie ref - musimy użyć .value
41 if (mainTankLevel.value > 0) {
42 mainTankLevel.value -= 0.5
43 }
44
45 // Używanie reactive - bezpośredni dostęp do właściwości
46 if (oxygenSystem.mainTank.level > 0) {
47 oxygenSystem.mainTank.level -= 0.5
48
49 if (oxygenSystem.mainTank.level < 30) {
50 oxygenSystem.mainTank.status = 'WARNING'
51 }
52 if (oxygenSystem.mainTank.level < 10) {
53 oxygenSystem.mainTank.status = 'CRITICAL'
54 }
55 }
56 }, 100)
57}
58
59function stopSimulation() {
60 if (simulationInterval) {
61 clearInterval(simulationInterval)
62 simulationInterval = null
63 }
64}
65
66function refill() {
67 // ref - używamy .value
68 mainTankLevel.value = 100
69 backupTankLevel.value = 75
70
71 // reactive - bezpośredni dostęp
72 oxygenSystem.mainTank.level = 100
73 oxygenSystem.mainTank.status = 'OPTIMAL'
74 oxygenSystem.backupTank.level = 75
75}
76
77// Przykład: nie możemy zastąpić całego obiektu reactive
78function wrongWayToUpdate() {
79 // TO NIE ZADZIAŁA - straci reaktywność
80 // oxygenSystem = { ...newData }
81
82 // PRAWIDŁOWY SPOSÓB - aktualizuj właściwości
83 oxygenSystem.mainTank.level = 100
84 oxygenSystem.mainTank.status = 'OPTIMAL'
85}
86</script>
87
88<template>
89 <div class="oxygen-control">
90 <div class="header">
91 <h1>SYSTEM TLENOWY - NOVA LAB</h1>
92 <p class="subtitle">ref() vs reactive() - Porównanie</p>
93 </div>
94
95 <div class="comparison-grid">
96 <!-- Sekcja ref() -->
97 <div class="section">
98 <h2 class="section-title">UŻYWAJĄC ref()</h2>
99 <div class="card">
100 <div class="tank-visual">
101 <div class="tank-fill" :style="{ height: mainTankLevel + '%' }">
102 <span class="tank-label">{{ mainTankLevel.toFixed(1) }}%</span>
103 </div>
104 </div>
105 <div class="tank-info">
106 <p><strong>Zbiornik główny:</strong> {{ mainTankLevel.toFixed(1) }}%</p>
107 <p><strong>Zbiornik zapasowy:</strong> {{ backupTankLevel.toFixed(1) }}%</p>
108 <p><strong>Przepływ:</strong> {{ oxygenFlow }} L/min</p>
109 </div>
110 <div class="code-example">
111 <pre>const level = ref(100)
112// Dostęp: level.value
113level.value = 50</pre>
114 </div>
115 </div>
116 </div>
117
118 <!-- Sekcja reactive() -->
119 <div class="section">
120 <h2 class="section-title">UŻYWAJĄC reactive()</h2>
121 <div class="card">
122 <div class="tank-visual">
123 <div
124 class="tank-fill"
125 :style="{ height: oxygenSystem.mainTank.level + '%' }"
126 :class="oxygenSystem.mainTank.status.toLowerCase()"
127 >
128 <span class="tank-label">{{ oxygenSystem.mainTank.level.toFixed(1) }}%</span>
129 </div>
130 </div>
131 <div class="tank-info">
132 <p>
133 <strong>Status:</strong>
134 <span :class="'status-' + oxygenSystem.mainTank.status.toLowerCase()">
135 {{ oxygenSystem.mainTank.status }}
136 </span>
137 </p>
138 <p><strong>Pojemność:</strong> {{ oxygenSystem.mainTank.capacity }} L</p>
139 <p><strong>Ciśnienie:</strong> {{ oxygenSystem.mainTank.pressure }} bar</p>
140 <p><strong>Przepływ:</strong> {{ oxygenSystem.flow.rate }} L/min</p>
141 <p><strong>Konsumenci:</strong> {{ oxygenSystem.flow.consumers }}</p>
142 </div>
143 <div class="code-example">
144 <pre>const system = reactive({
145 mainTank: { level: 100 }
146})
147// Dostęp: system.mainTank.level
148system.mainTank.level = 50</pre>
149 </div>
150 </div>
151 </div>
152 </div>
153
154 <div class="controls">
155 <button @click="startSimulation" class="btn btn-start">
156 ▶START SYMULACJI
157 </button>
158 <button @click="stopSimulation" class="btn btn-stop">
159 ⏸STOP
160 </button>
161 <button @click="refill" class="btn btn-refill">
162 UZUPEŁNIJ
163 </button>
164 </div>
165
166 <div class="info-box">
167 <h3>Kiedy używać ref() vs reactive()?</h3>
168 <div class="comparison-table">
169 <div class="comparison-col">
170 <h4>ref()</h4>
171 <ul>
172 <li>Wartości prymitywne (string, number, boolean)</li>
173 <li>Gdy chcesz zastąpić całą wartość</li>
174 <li>Łatwiejsze do debugowania</li>
175 <li>Wymaga .value w script</li>
176 </ul>
177 </div>
178 <div class="comparison-col">
179 <h4>reactive()</h4>
180 <ul>
181 <li>Obiekty i tablice</li>
182 <li>Grupowanie powiązanych danych</li>
183 <li>Nie wymaga .value</li>
184 <li>Nie można zastąpić całego obiektu</li>
185 </ul>
186 </div>
187 </div>
188 </div>
189 </div>
190</template>
191
192<style scoped>
193.oxygen-control {
194 min-height: 100vh;
195 background: #0a0e27;
196 color: #e0e0e0;
197 padding: 2rem;
198 font-family: 'Courier New', monospace;
199}
200
201.header {
202 text-align: center;
203 margin-bottom: 2rem;
204 border-bottom: 2px solid #00ff88;
205 padding-bottom: 1rem;
206}
207
208.header h1 {
209 color: #00ff88;
210 font-size: 2rem;
211 margin: 0;
212 text-shadow: 0 0 10px #00ff88;
213}
214
215.subtitle {
216 color: #00b4d8;
217 margin: 0.5rem 0 0 0;
218}
219
220.comparison-grid {
221 display: grid;
222 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
223 gap: 2rem;
224 margin-bottom: 2rem;
225}
226
227.section-title {
228 color: #00b4d8;
229 font-size: 1.3rem;
230 margin-bottom: 1rem;
231 text-align: center;
232}
233
234.card {
235 background: #1a1f3a;
236 padding: 1.5rem;
237 border-radius: 8px;
238 border: 1px solid #00b4d8;
239}
240
241.tank-visual {
242 width: 100%;
243 height: 200px;
244 background: #0a0e27;
245 border: 2px solid #00b4d8;
246 border-radius: 8px;
247 position: relative;
248 overflow: hidden;
249 margin-bottom: 1rem;
250}
251
252.tank-fill {
253 position: absolute;
254 bottom: 0;
255 width: 100%;
256 background: linear-gradient(180deg, #00ff88, #00b4d8);
257 transition: height 0.3s ease;
258 display: flex;
259 align-items: center;
260 justify-content: center;
261}
262
263.tank-fill.warning {
264 background: linear-gradient(180deg, #ffaa00, #ff8800);
265}
266
267.tank-fill.critical {
268 background: linear-gradient(180deg, #ff4444, #cc0000);
269}
270
271.tank-label {
272 color: #0a0e27;
273 font-weight: bold;
274 font-size: 1.5rem;
275 text-shadow: 0 0 5px rgba(255,255,255,0.5);
276}
277
278.tank-info {
279 margin-bottom: 1rem;
280}
281
282.tank-info p {
283 margin: 0.5rem 0;
284 color: #ccc;
285}
286
287.status-optimal {
288 color: #00ff88;
289}
290
291.status-warning {
292 color: #ffaa00;
293}
294
295.status-critical {
296 color: #ff4444;
297 animation: blink 1s infinite;
298}
299
300@keyframes blink {
301 0%, 50%, 100% { opacity: 1; }
302 25%, 75% { opacity: 0.5; }
303}
304
305.code-example {
306 background: #0a0e27;
307 padding: 1rem;
308 border-radius: 4px;
309 border-left: 3px solid #00ff88;
310}
311
312.code-example pre {
313 margin: 0;
314 color: #00ff88;
315 font-size: 0.85rem;
316 overflow-x: auto;
317}
318
319.controls {
320 display: flex;
321 gap: 1rem;
322 margin-bottom: 2rem;
323 justify-content: center;
324 flex-wrap: wrap;
325}
326
327.btn {
328 padding: 1rem 2rem;
329 font-size: 1rem;
330 border: none;
331 border-radius: 8px;
332 cursor: pointer;
333 font-weight: bold;
334 transition: all 0.3s ease;
335 font-family: 'Courier New', monospace;
336}
337
338.btn-start {
339 background: linear-gradient(135deg, #00ff88, #00b4d8);
340 color: #0a0e27;
341}
342
343.btn-stop {
344 background: linear-gradient(135deg, #ffaa00, #ff8800);
345 color: #0a0e27;
346}
347
348.btn-refill {
349 background: linear-gradient(135deg, #00b4d8, #0088cc);
350 color: white;
351}
352
353.btn:hover {
354 transform: scale(1.05);
355 box-shadow: 0 0 20px currentColor;
356}
357
358.info-box {
359 background: #1a1f3a;
360 padding: 1.5rem;
361 border-radius: 8px;
362 border-left: 4px solid #00ff88;
363}
364
365.info-box h3 {
366 color: #00ff88;
367 margin-top: 0;
368}
369
370.comparison-table {
371 display: grid;
372 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
373 gap: 1.5rem;
374 margin-top: 1rem;
375}
376
377.comparison-col h4 {
378 color: #00b4d8;
379 margin-top: 0;
380}
381
382.comparison-col ul {
383 padding-left: 1.5rem;
384 color: #ccc;
385}
386
387.comparison-col li {
388 margin: 0.5rem 0;
389}
390</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. Do czego służy reactive() w Vue 3?
2. Czy reactive() wymaga .value do odczytu właściwości?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Użyj reactive() do stworzenia obiektu z wieloma właściwościami i wyświetl je.
- Układanie w poziomie
Ułóż poprawną składnię tworzenia obiektu reactive:
- Edytor kodu
Stwórz reactive() i dodaj przyciski modyfikujące jego właściwości.
- Klikanie w kolejności
Ułóż kroki tworzenia i aktualizacji obiektu reactive:
- Układanie w pionie
Ułóż workflow pracy z reactive():