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

reactive() - Reaktywne Systemy

4 min czytania
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ę panelu

Proxy 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) // true

Proxy 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:

Cecharef()reactive()
Dla prymitywówTakNie
Dla obiektówTak (.value)Tak (bezpośrednio)
DestructuringBezpieczne (przekazujesz cały kontener)Traci reaktywność
ReassignTak (.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 reaktywny

Destrukturyzacja 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. 1. Do czego służy reactive() w Vue 3?

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

Przydatne artykuły