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

toRef i toRefs - Bezpieczne Rozpakowywanie Sensorów

4 min czytania
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) // 2600

toRefs 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) // 5000

Obiekt 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. 1. Co się stanie przy destructuringu obiektu reactive: const { name } = state?

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

Przydatne artykuły