Vue.js course ยท Module 3: Reactivity

toRef and toRefs - Safe Sensor Unpacking

5 min read
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) // 2600

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

The 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. 1. What happens when you destructure a reactive object?

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

Useful articles