Vue.js course ยท Module 3: Reactivity
toRef and toRefs - Safe Sensor Unpacking
In this lesson6
Sometimes you need to extract individual properties from a reactive sensor object while preserving reactivity - like isolating specific readings from a complex monitoring system.
You want to pass just the temperature to a display function, or give fields shorter names in the template, so you reach for destructuring. It looks innocent, but from a reactive object it takes only a snapshot of a reading at one moment. This lesson shows how to take a property out together with the wire that keeps carrying changes.
The Destructuring Problem
See what happens to values taken out of a reactive() object when the object changes later:
1import { reactive } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// BAD - loses reactivity!
9const { name, temperature } = reactor
10console.log(name) // 'Plasma Core Alpha' - but not reactive
11
12reactor.name = 'Plasma Core Beta'
13console.log(name) // Still 'Plasma Core Alpha'!The name variable received a plain string copied at the moment of destructuring. The reactor object changed correctly, but the copy does not know about it and will not refresh any view. The same happens when you pass reactor.temperature to a function: it receives a number, not a connection to the reactor.
toRef() - Single Sensor
toRef(object, 'key') creates a ref linked to a single property. The connection works both ways:
1import { reactive, toRef } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// toRef creates a reactive reference to a reading
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' - synchronized!
15
16nameRef.value = 'Plasma Core Gamma'
17console.log(reactor.name) // 'Plasma Core Gamma' - bidirectional!nameRef does not store its own copy; it reads and writes reactor.name. This is different from ref(reactor.name), which would receive a plain string and be synchronized with nothing.
toRefs() - All Sensors
toRefs() does the same for all properties at once and returns a plain object full of refs that can be destructured safely:
1import { reactive, toRefs } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500,
6 status: 'ACTIVE'
7})
8
9// toRefs converts all properties
10const { name, temperature, status } = toRefs(reactor)
11
12// Each is now a ref with .value
13console.log(name.value) // 'Plasma Core Alpha'
14console.log(temperature.value) // 2500
15
16// Changes are synchronized
17reactor.temperature = 2600
18console.log(temperature.value) // 2600toRefs creates refs only for the properties that exist at the moment of the call. A field added later will not get a ref - for an optional field use toRef. The same mechanism helps when a function returns state: returning toRefs(state) lets the caller destructure the result without losing reactivity.
Usage in Components
The station archive keeps holographic copies of Earth's works of art. The edit form uses v-model, the two-way binding directive: typed text goes into the data, and a change of the data goes back into the field. You will learn it in detail in module 5.
1<template>
2 <div>
3 <input v-model="title" />
4 <p>Year: {{ 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// Unpack for use in template
17const { title, year } = toRefs(artwork)
18</script>Typing into the field changes artwork.title through the title ref. In the template you could just as well write artwork.title - toRefs is most useful when you return state from a function or pass it on.
toRef with Default Value
The third argument of toRef is a default value, used when the property is undefined:
1import { reactive, toRef } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha'
5 // no 'maxTemperature'
6})
7
8// With default value (Vue 3.2.25+)
9const maxTemperature = toRef(reactor, 'maxTemperature', 5000)
10console.log(maxTemperature.value) // 5000The reactor object did not gain a new field - the default value lives only in the ref. It comes in handy with telemetry batches in which some fields are empty.
Practical Example - Props
Props are data a component receives from its parent. The defineProps macro declares them without an import, and a type written in angle brackets requires 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// Unpack props while preserving reactivity
11const { sensorName, reading } = toRefs(props)
12
13// Now you can use them in watch, computed, etc.
14</script>The props object is reactive, so toRefs gives you refs that keep up with the parent. Since Vue 3.5 even variables destructured directly from defineProps() stay reactive, because the compiler turns them into props.sensorName. When you pass such a prop to another function, however, you still need a ref or a getter like () => props.reading. You will get to know components and props in module 6.
My choice: toRef for one field, toRefs for a whole object, and the plain dot notation in a simple template. In the next lesson you will meet tools that tell you whether a value is a ref, a reactive object or just a plain copy.
Remember: destructuring takes a snapshot of a reading, while toRef and toRefs connect a wire that carries changes both ways.
Code for this lesson: App.vue
1<script setup>
2import { ref, reactive, isRef, isReactive, unref } from 'vue'
3
4// System diagnostics - isRef(), isReactive(), unref()
5// Checking reactivity types in NOVA LAB
6
7// Different data types
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// Diagnostic function
27function diagnoseValue(value, name) {
28 return {
29 name,
30 isRef: isRef(value),
31 isReactive: isReactive(value),
32 rawValue: unref(value), // unref() returns value without .value
33 type: typeof value
34 }
35}
36
37// Diagnostic results
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// Example of using unref()
54function getValue(maybeRef) {
55 // unref() safely extracts value from ref or returns normal value
56 return unref(maybeRef)
57}
58
59// Universal function accepting ref or value
60function updatePowerLevel(newLevel) {
61 // Check if it is a ref
62 if (isRef(powerLevel)) {
63 powerLevel.value = newLevel
64 } else {
65 console.log('powerLevel is not a ref!')
66 }
67}
68
69// Universal function for objects
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// Practical examples
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 // We use unref() to safely get the value
87 const value = unref(item.value)
88 return value * 1.1
89}
90
91// Run diagnostics on load
92runDiagnostics()
93</script>
94
95<template>
96 <div class="diagnostics">
97 <div class="header">
98 <h1>SYSTEM DIAGNOSTICS - NOVA LAB</h1>
99 <p class="subtitle">isRef(), isReactive(), unref() - Checking reactivity</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>Reactivity Diagnostics Results</h2>
146 <button @click="runDiagnostics" class="btn btn-scan">
147 Run again
148 </button>
149 </div>
150
151 <div class="results-table">
152 <div class="table-header">
153 <div>Variable name</div>
154 <div>isRef()</div>
155 <div>isReactive()</div>
156 <div>Value (unref)</div>
157 <div>Type</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>Practical examples</h2>
183
184 <div class="example-grid">
185 <div class="example-card">
186 <h3>unref() - Universal value retrieval</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() - Checking if a value is a ref</h3>
206 <div class="code-box">
207 <pre>if (isRef(value)) {
208 value.value = newValue
209} else {
210 // handle normal value
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 Set power to 90%
218 </button>
219 </div>
220 </div>
221
222 <div class="example-card">
223 <h3>isReactive() - Checking if an object is reactive</h3>
224 <div class="code-box">
225 <pre>if (isReactive(obj)) {
226 obj.property = newValue
227} else {
228 // handle normal object
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>Mixed data - practical case</h3>
242 <div class="code-box">
243 <pre>// Some values are ref, others are not
244const sensors = [
245 { name: 'A', value: ref(100) },
246 { name: 'B', value: 200 }
247]
248
249// unref() handles both cases
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>When to use these functions?</h3>
268 <ul>
269 <li>
270 <strong>isRef(value):</strong> Checks if a value is a ref. Useful in universal functions.
271 </li>
272 <li>
273 <strong>isReactive(value):</strong> Checks if an object is reactive. Helps with debugging.
274 </li>
275 <li>
276 <strong>unref(value):</strong> Returns the value from a ref or the original value. Safe way to access.
277 </li>
278 <li>
279 <strong>Practical use:</strong> Creating functions compatible with both ref and normal values.
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>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What happens when you destructure a reactive object?
2. What does toRef() do?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a reactive object and use toRef() to create a ref linked to a single property.
- Click in order
Arrange the elements of toRef() syntax in the correct order.
- Code editor
Create a reactive object and use toRefs() for safe destructuring that preserves reactivity.
- Vertical ordering
Arrange the steps for safe destructuring with toRefs() in the correct order.
- Horizontal ordering
Arrange the elements of destructuring with toRefs in the correct order.