Kurs Vue.js · Moduł 3: Reaktywność
toRef i toRefs - Bezpieczne Rozpakowywanie Sensorów
W tej lekcji6
Czasem potrzebujesz wyciągnąć pojedyncze właściwości z reaktywnego obiektu sensora, zachowując reaktywność - jak wydzielenie konkretnych odczytów z kompleksowego systemu monitoringu.
Chcesz przekazać samą temperaturę do funkcji wyświetlacza albo nadać polom krótsze nazwy w szablonie, więc sięgasz po destrukturyzację. Wygląda niewinnie, ale z obiektu reaktywnego wyjmuje tylko zdjęcie odczytu z jednej chwili. Ta lekcja pokazuje, jak wyjąć właściwość razem z przewodem, który dalej przewodzi zmiany.
Problem z Destructuring
Zobacz, co się dzieje z wartościami wyjętymi z obiektu reactive(), gdy obiekt później się zmieni:
1import { reactive } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// ŹLE - traci reaktywność!
9const { name, temperature } = reactor
10console.log(name) // 'Plasma Core Alpha' - ale nie jest reaktywny
11
12reactor.name = 'Plasma Core Beta'
13console.log(name) // Nadal 'Plasma Core Alpha'!Zmienna name dostała zwykły napis skopiowany w chwili destrukturyzacji. Obiekt reactor zmienił się poprawnie, ale kopia o tym nie wie i nie odświeży żadnego widoku. To samo stanie się, gdy przekażesz reactor.temperature do funkcji: dostanie ona liczbę, a nie połączenie z reaktorem.
toRef() - Pojedynczy Sensor
toRef(obiekt, 'klucz') tworzy ref powiązany z jedną właściwością. Połączenie działa w obie strony:
1import { reactive, toRef } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// toRef tworzy reaktywną referencję do odczytu
9const nameRef = toRef(reactor, 'name')
10
11console.log(nameRef.value) // 'Plasma Core Alpha'
12
13reactor.name = 'Plasma Core Beta'
14console.log(nameRef.value) // 'Plasma Core Beta' - zsynchronizowane!
15
16nameRef.value = 'Plasma Core Gamma'
17console.log(reactor.name) // 'Plasma Core Gamma' - dwukierunkowe!nameRef nie przechowuje własnej kopii, tylko czyta i zapisuje reactor.name. To inna sytuacja niż ref(reactor.name), które dostałoby zwykły napis i nie byłoby z niczym zsynchronizowane.
toRefs() - Wszystkie Sensory
toRefs() robi to samo naraz dla wszystkich właściwości i zwraca zwykły obiekt pełen refów, który można bezpiecznie zdestrukturyzować:
1import { reactive, toRefs } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500,
6 status: 'ACTIVE'
7})
8
9// toRefs konwertuje wszystkie właściwości
10const { name, temperature, status } = toRefs(reactor)
11
12// Każda jest teraz ref z .value
13console.log(name.value) // 'Plasma Core Alpha'
14console.log(temperature.value) // 2500
15
16// Zmiany są zsynchronizowane
17reactor.temperature = 2600
18console.log(temperature.value) // 2600toRefs tworzy refy tylko dla właściwości, które istnieją w chwili wywołania. Pole dodane później nie dostanie refa - dla pola opcjonalnego użyj toRef. Ten sam mechanizm przydaje się, gdy funkcja zwraca stan: zwrócenie toRefs(state) pozwala odbiorcy zdestrukturyzować wynik bez utraty reaktywności.
Użycie w Komponentach
W archiwum stacji przechowujemy holograficzne kopie ziemskich dzieł sztuki. Formularz edycji używa v-model, dyrektywy wiązania dwukierunkowego: wpisany tekst trafia do danych, a zmiana danych wraca do pola. Szczegółowo poznasz ją w module 5.
1<template>
2 <div>
3 <input v-model="title" />
4 <p>Rok: {{ year }}</p>
5 </div>
6</template>
7
8<script setup>
9import { reactive, toRefs } from 'vue'
10
11const artwork = reactive({
12 title: 'Mona Lisa',
13 year: 1503
14})
15
16// Rozpakuj do użycia w template
17const { title, year } = toRefs(artwork)
18</script>Wpisywanie w pole zmienia artwork.title przez ref title. W szablonie mógłbyś równie dobrze pisać artwork.title - toRefs najbardziej przydaje się wtedy, gdy zwracasz stan z funkcji albo przekazujesz go dalej.
toRef z Domyślną Wartością
Trzeci argument toRef to wartość domyślna, używana wtedy, gdy właściwość ma wartość undefined:
1import { reactive, toRef } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha'
5 // brak 'maxTemperature'
6})
7
8// Z wartością domyślną (Vue 3.2.25+)
9const maxTemperature = toRef(reactor, 'maxTemperature', 5000)
10console.log(maxTemperature.value) // 5000Obiekt reactor nie zyskał przy tym nowego pola - wartość domyślna żyje tylko w refie. Przydaje się przy paczkach telemetrii, w których część pól bywa pusta.
Praktyczny Przykład - Props
Props to dane, które komponent dostaje od rodzica. Makro defineProps deklaruje je bez importu, a zapis typu w nawiasach ostrych wymaga lang="ts":
1<!-- SensorDisplay.vue -->
2<script setup lang="ts">
3import { toRefs } from 'vue'
4
5const props = defineProps<{
6 sensorName: string
7 reading: number
8}>()
9
10// Rozpakuj props z zachowaniem reaktywności
11const { sensorName, reading } = toRefs(props)
12
13// Teraz możesz użyć w watch, computed itp.
14</script>Obiekt props jest reaktywny, więc toRefs daje refy, które nadążają za rodzicem. Od Vue 3.5 nawet zmienne zdestrukturyzowane wprost z defineProps() pozostają reaktywne, bo kompilator zamienia je na props.sensorName. Gdy jednak przekazujesz taki props do innej funkcji, nadal potrzebujesz refa albo gettera () => props.reading. Komponenty i props poznasz w module 6.
Mój wybór: toRef dla jednego pola, toRefs dla całego obiektu, a w prostym szablonie zwykły zapis z kropką. W następnej lekcji poznasz narzędzia, które powiedzą Ci, czy dana wartość jest refem, obiektem reaktywnym, czy zwykłą kopią.
Zapamiętaj: destrukturyzacja robi zdjęcie odczytu, a toRef i toRefs podłączają przewód, który przewodzi zmiany w obie strony.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, reactive, isRef, isReactive, unref } from 'vue'
3
4// Diagnostyka systemów - isRef(), isReactive(), unref()
5// Sprawdzanie typów reaktywności w NOVA LAB
6
7// Różne typy danych
8const systemName = ref('Plasma Core System')
9const isPowered = ref(true)
10const powerLevel = ref(75)
11
12const systemConfig = reactive({
13 id: 'SYS-001',
14 type: 'REACTOR',
15 online: true,
16 sensors: {
17 temperature: 850,
18 pressure: 120
19 }
20})
21
22const normalString = 'This is not reactive'
23const normalNumber = 42
24const normalObject = { test: 'value' }
25
26// Funkcja diagnostyczna
27function diagnoseValue(value, name) {
28 return {
29 name,
30 isRef: isRef(value),
31 isReactive: isReactive(value),
32 rawValue: unref(value), // unref() zwraca wartość bez .value
33 type: typeof value
34 }
35}
36
37// Wyniki diagnostyki
38const diagnosticResults = ref([])
39
40function runDiagnostics() {
41 diagnosticResults.value = [
42 diagnoseValue(systemName, 'systemName'),
43 diagnoseValue(isPowered, 'isPowered'),
44 diagnoseValue(powerLevel, 'powerLevel'),
45 diagnoseValue(systemConfig, 'systemConfig'),
46 diagnoseValue(systemConfig.sensors, 'systemConfig.sensors'),
47 diagnoseValue(normalString, 'normalString'),
48 diagnoseValue(normalNumber, 'normalNumber'),
49 diagnoseValue(normalObject, 'normalObject')
50 ]
51}
52
53// Przykład użycia unref()
54function getValue(maybeRef) {
55 // unref() bezpiecznie wyciąga wartość z ref lub zwraca normalną wartość
56 return unref(maybeRef)
57}
58
59// Funkcja uniwersalna przyjmująca ref lub wartość
60function updatePowerLevel(newLevel) {
61 // Sprawdzamy czy to ref
62 if (isRef(powerLevel)) {
63 powerLevel.value = newLevel
64 } else {
65 console.log('powerLevel is not a ref!')
66 }
67}
68
69// Funkcja uniwersalna dla obiektów
70function updateSystemStatus(system, status) {
71 if (isReactive(system)) {
72 system.online = status
73 } else {
74 console.log('System is not reactive!')
75 }
76}
77
78// Przykłady praktyczne
79const mixedData = ref([
80 { name: 'Sensor A', value: ref(100) },
81 { name: 'Sensor B', value: 200 },
82 { name: 'Sensor C', value: ref(300) }
83])
84
85function processValue(item) {
86 // Używamy unref() aby bezpiecznie pobrać wartość
87 const value = unref(item.value)
88 return value * 1.1
89}
90
91// Uruchom diagnostykę przy załadowaniu
92runDiagnostics()
93</script>
94
95<template>
96 <div class="diagnostics">
97 <div class="header">
98 <h1>DIAGNOSTYKA SYSTEMÓW - NOVA LAB</h1>
99 <p class="subtitle">isRef(), isReactive(), unref() - Sprawdzanie reaktywności</p>
100 </div>
101
102 <div class="system-panel">
103 <h2>System Status</h2>
104 <div class="status-grid">
105 <div class="status-item">
106 <div class="label">System Name</div>
107 <div class="value">{{ systemName }}</div>
108 <div class="type-badge ref">REF</div>
109 </div>
110
111 <div class="status-item">
112 <div class="label">Power Status</div>
113 <div class="value">{{ isPowered ? 'ONLINE' : 'OFFLINE' }}</div>
114 <div class="type-badge ref">REF</div>
115 </div>
116
117 <div class="status-item">
118 <div class="label">Power Level</div>
119 <div class="value">{{ powerLevel }}%</div>
120 <div class="type-badge ref">REF</div>
121 </div>
122
123 <div class="status-item">
124 <div class="label">System ID</div>
125 <div class="value">{{ systemConfig.id }}</div>
126 <div class="type-badge reactive">REACTIVE</div>
127 </div>
128
129 <div class="status-item">
130 <div class="label">Temperature</div>
131 <div class="value">{{ systemConfig.sensors.temperature }}°C</div>
132 <div class="type-badge reactive">REACTIVE</div>
133 </div>
134
135 <div class="status-item">
136 <div class="label">Pressure</div>
137 <div class="value">{{ systemConfig.sensors.pressure }} bar</div>
138 <div class="type-badge reactive">REACTIVE</div>
139 </div>
140 </div>
141 </div>
142
143 <div class="diagnostics-panel">
144 <div class="panel-header">
145 <h2>Wyniki Diagnostyki Reaktywności</h2>
146 <button @click="runDiagnostics" class="btn btn-scan">
147 Uruchom ponownie
148 </button>
149 </div>
150
151 <div class="results-table">
152 <div class="table-header">
153 <div>Nazwa zmiennej</div>
154 <div>isRef()</div>
155 <div>isReactive()</div>
156 <div>Wartość (unref)</div>
157 <div>Typ</div>
158 </div>
159 <div
160 v-for="result in diagnosticResults"
161 :key="result.name"
162 class="table-row"
163 >
164 <div class="cell-name">{{ result.name }}</div>
165 <div class="cell-check">
166 <span v-if="result.isRef" class="check-yes">TRUE</span>
167 <span v-else class="check-no">FALSE</span>
168 </div>
169 <div class="cell-check">
170 <span v-if="result.isReactive" class="check-yes">TRUE</span>
171 <span v-else class="check-no">FALSE</span>
172 </div>
173 <div class="cell-value">
174 {{ typeof result.rawValue === 'object' ? JSON.stringify(result.rawValue) : result.rawValue }}
175 </div>
176 <div class="cell-type">{{ result.type }}</div>
177 </div>
178 </div>
179 </div>
180
181 <div class="examples-section">
182 <h2>Przykłady praktyczne</h2>
183
184 <div class="example-grid">
185 <div class="example-card">
186 <h3>unref() - Uniwersalne pobieranie wartości</h3>
187 <div class="code-box">
188 <pre>function getValue(maybeRef) {
189 return unref(maybeRef)
190}
191
192const refValue = ref(100)
193const normalValue = 200
194
195getValue(refValue) // 100
196getValue(normalValue) // 200</pre>
197 </div>
198 <div class="demo">
199 <p>getValue(powerLevel): <strong>{{ getValue(powerLevel) }}</strong></p>
200 <p>getValue(normalNumber): <strong>{{ getValue(normalNumber) }}</strong></p>
201 </div>
202 </div>
203
204 <div class="example-card">
205 <h3>isRef() - Sprawdzanie czy wartość jest ref</h3>
206 <div class="code-box">
207 <pre>if (isRef(value)) {
208 value.value = newValue
209} else {
210 // obsłuż normalną wartość
211}</pre>
212 </div>
213 <div class="demo">
214 <p>isRef(powerLevel): <strong class="check-yes">{{ isRef(powerLevel) }}</strong></p>
215 <p>isRef(normalNumber): <strong class="check-no">{{ isRef(normalNumber) }}</strong></p>
216 <button @click="updatePowerLevel(90)" class="btn btn-small">
217 Ustaw moc na 90%
218 </button>
219 </div>
220 </div>
221
222 <div class="example-card">
223 <h3>isReactive() - Sprawdzanie czy obiekt jest reactive</h3>
224 <div class="code-box">
225 <pre>if (isReactive(obj)) {
226 obj.property = newValue
227} else {
228 // obsłuż normalny obiekt
229}</pre>
230 </div>
231 <div class="demo">
232 <p>isReactive(systemConfig): <strong class="check-yes">{{ isReactive(systemConfig) }}</strong></p>
233 <p>isReactive(normalObject): <strong class="check-no">{{ isReactive(normalObject) }}</strong></p>
234 <button @click="updateSystemStatus(systemConfig, !systemConfig.online)" class="btn btn-small">
235 Toggle system status
236 </button>
237 </div>
238 </div>
239
240 <div class="example-card">
241 <h3>Mieszane dane - praktyczny przypadek</h3>
242 <div class="code-box">
243 <pre>// Niektóre wartości to ref, inne nie
244const sensors = [
245 { name: 'A', value: ref(100) },
246 { name: 'B', value: 200 }
247]
248
249// unref() radzi sobie z obiema
250sensors.forEach(s => {
251 const val = unref(s.value)
252})</pre>
253 </div>
254 <div class="demo">
255 <div v-for="item in mixedData" :key="item.name" class="sensor-item">
256 <span>{{ item.name }}:</span>
257 <span class="sensor-value">{{ processValue(item) }}</span>
258 <span v-if="isRef(item.value)" class="type-badge ref">REF</span>
259 <span v-else class="type-badge normal">NORMAL</span>
260 </div>
261 </div>
262 </div>
263 </div>
264 </div>
265
266 <div class="info-box">
267 <h3>Kiedy używać tych funkcji?</h3>
268 <ul>
269 <li>
270 <strong>isRef(value):</strong> Sprawdza czy wartość jest ref. Przydatne w funkcjach uniwersalnych.
271 </li>
272 <li>
273 <strong>isReactive(value):</strong> Sprawdza czy obiekt jest reactive. Pomaga w debugowaniu.
274 </li>
275 <li>
276 <strong>unref(value):</strong> Zwraca wartość z ref lub oryginalną wartość. Bezpieczny sposób na dostęp.
277 </li>
278 <li>
279 <strong>Praktyczne zastosowanie:</strong> Tworzenie funkcji kompatybilnych z ref i normalnymi wartościami.
280 </li>
281 </ul>
282 </div>
283 </div>
284</template>
285
286<style scoped>
287.diagnostics {
288 min-height: 100vh;
289 background: #0a0e27;
290 color: #e0e0e0;
291 padding: 2rem;
292 font-family: 'Courier New', monospace;
293}
294
295.header {
296 text-align: center;
297 margin-bottom: 2rem;
298 border-bottom: 2px solid #00ff88;
299 padding-bottom: 1rem;
300}
301
302.header h1 {
303 color: #00ff88;
304 font-size: 2rem;
305 margin: 0;
306 text-shadow: 0 0 10px #00ff88;
307}
308
309.subtitle {
310 color: #00b4d8;
311 margin: 0.5rem 0 0 0;
312}
313
314.system-panel {
315 background: #1a1f3a;
316 padding: 1.5rem;
317 border-radius: 8px;
318 border: 1px solid #00b4d8;
319 margin-bottom: 2rem;
320}
321
322.system-panel h2 {
323 color: #00b4d8;
324 margin-top: 0;
325}
326
327.status-grid {
328 display: grid;
329 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
330 gap: 1rem;
331}
332
333.status-item {
334 background: #0a0e27;
335 padding: 1rem;
336 border-radius: 6px;
337 position: relative;
338}
339
340.label {
341 font-size: 0.85rem;
342 color: #888;
343 margin-bottom: 0.5rem;
344}
345
346.value {
347 font-size: 1.3rem;
348 color: #00ff88;
349 font-weight: bold;
350}
351
352.type-badge {
353 position: absolute;
354 top: 0.5rem;
355 right: 0.5rem;
356 padding: 0.2rem 0.5rem;
357 border-radius: 4px;
358 font-size: 0.7rem;
359 font-weight: bold;
360}
361
362.type-badge.ref {
363 background: #00b4d8;
364 color: #0a0e27;
365}
366
367.type-badge.reactive {
368 background: #00ff88;
369 color: #0a0e27;
370}
371
372.type-badge.normal {
373 background: #666;
374 color: white;
375}
376
377.diagnostics-panel {
378 background: #1a1f3a;
379 padding: 1.5rem;
380 border-radius: 8px;
381 border: 1px solid #00ff88;
382 margin-bottom: 2rem;
383}
384
385.panel-header {
386 display: flex;
387 justify-content: space-between;
388 align-items: center;
389 margin-bottom: 1.5rem;
390}
391
392.panel-header h2 {
393 color: #00ff88;
394 margin: 0;
395}
396
397.btn {
398 padding: 0.6rem 1.2rem;
399 font-size: 0.9rem;
400 border: none;
401 border-radius: 6px;
402 cursor: pointer;
403 font-weight: bold;
404 transition: all 0.3s ease;
405 font-family: 'Courier New', monospace;
406}
407
408.btn-scan {
409 background: linear-gradient(135deg, #00ff88, #00b4d8);
410 color: #0a0e27;
411}
412
413.btn-scan:hover {
414 transform: scale(1.05);
415 box-shadow: 0 0 20px #00ff88;
416}
417
418.btn-small {
419 padding: 0.5rem 1rem;
420 font-size: 0.85rem;
421}
422
423.results-table {
424 background: #0a0e27;
425 border-radius: 6px;
426 overflow: hidden;
427}
428
429.table-header, .table-row {
430 display: grid;
431 grid-template-columns: 2fr 1fr 1fr 2fr 1fr;
432 gap: 1rem;
433 padding: 1rem;
434 align-items: center;
435}
436
437.table-header {
438 background: #00ff8820;
439 color: #00ff88;
440 font-weight: bold;
441 border-bottom: 2px solid #00ff88;
442}
443
444.table-row {
445 border-bottom: 1px solid #333;
446}
447
448.table-row:hover {
449 background: #1a1f3a;
450}
451
452.cell-name {
453 color: #00b4d8;
454 font-family: monospace;
455}
456
457.cell-check {
458 text-align: center;
459}
460
461.check-yes {
462 color: #00ff88;
463}
464
465.check-no {
466 color: #666;
467}
468
469.cell-value {
470 color: #ccc;
471 font-size: 0.9rem;
472 overflow: hidden;
473 text-overflow: ellipsis;
474}
475
476.cell-type {
477 color: #888;
478 font-style: italic;
479}
480
481.examples-section {
482 margin-bottom: 2rem;
483}
484
485.examples-section h2 {
486 color: #00ff88;
487 margin-bottom: 1.5rem;
488}
489
490.example-grid {
491 display: grid;
492 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
493 gap: 1.5rem;
494}
495
496.example-card {
497 background: #1a1f3a;
498 padding: 1.5rem;
499 border-radius: 8px;
500 border: 1px solid #333;
501}
502
503.example-card h3 {
504 color: #00b4d8;
505 margin-top: 0;
506 font-size: 1.1rem;
507}
508
509.code-box {
510 background: #0a0e27;
511 padding: 1rem;
512 border-radius: 4px;
513 border-left: 3px solid #00ff88;
514 margin: 1rem 0;
515}
516
517.code-box pre {
518 margin: 0;
519 color: #00ff88;
520 font-size: 0.85rem;
521 overflow-x: auto;
522}
523
524.demo {
525 background: #0a0e27;
526 padding: 1rem;
527 border-radius: 4px;
528}
529
530.demo p {
531 margin: 0.5rem 0;
532 color: #ccc;
533}
534
535.demo strong {
536 color: #00ff88;
537}
538
539.sensor-item {
540 display: flex;
541 justify-content: space-between;
542 align-items: center;
543 padding: 0.5rem;
544 margin: 0.3rem 0;
545 background: #1a1f3a;
546 border-radius: 4px;
547}
548
549.sensor-value {
550 color: #00ff88;
551 font-weight: bold;
552}
553
554.info-box {
555 background: #1a1f3a;
556 padding: 1.5rem;
557 border-radius: 8px;
558 border-left: 4px solid #00ff88;
559}
560
561.info-box h3 {
562 color: #00ff88;
563 margin-top: 0;
564}
565
566.info-box ul {
567 color: #ccc;
568 line-height: 1.8;
569}
570
571.info-box strong {
572 color: #00b4d8;
573}
574</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. Co się stanie przy destructuringu obiektu reactive: const { name } = state?
2. Co robi toRef(state, 'name')?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Użyj toRef() do stworzenia reaktywnej referencji do właściwości reactive.
- Klikanie w kolejności
Ułóż poprawne użycie toRef():
- Edytor kodu
Użyj toRefs() do bezpiecznego destructuringu obiektu reactive.
- Układanie w pionie
Ułóż kroki bezpiecznego destructuringu reactive:
- Układanie w poziomie
Ułóż składnię destructuringu z toRefs: