Vue.js course Β· Module 3: Reactivity

Debugging Reactivity

5 min read
In this lesson6

Even the best NOVA LAB engineers sometimes need to debug their systems. Learn the tools for debugging reactivity.

"The panel does not refresh" is the most common entry in the station's fault log. The cause almost always lies in the data: the value turns out to be a plain object instead of a Proxy, or it lost its connection after destructuring. Before you start rewriting the component, check what you are really working with - Vue has a set of diagnostic testers for that.

isRef() and isReactive()

isRef() returns true when the value is a ref. isReactive() checks whether an object is a Proxy created by reactive() or shallowReactive(). Both functions return a boolean:

1import { ref, reactive, isRef, isReactive } from 'vue'
2
3const powerLevel = ref(0)
4const reactor = reactive({ name: 'Plasma Core Alpha' })
5const plainConfig = { mode: 'safe' }
6
7console.log(isRef(powerLevel))       // true
8console.log(isRef(reactor))     // false
9console.log(isReactive(reactor)) // true
10console.log(isReactive(powerLevel))   // false
11console.log(isReactive(plainConfig)) // false

A ref is not a Proxy, which is why isReactive(powerLevel) gives false. An object stored inside a ref already is one: isReactive(ref({}).value) returns true. Related testers are isProxy(), which recognizes any Vue Proxy, including read-only ones, and isReadonly().

unref() - Safe Reading Unpacking

unref(x) is shorthand for isRef(x) ? x.value : x. It lets you write a function that accepts both a ref and a plain value:

1import { ref, unref } from 'vue'
2
3const temperature = ref(2500)
4const plainReading = 1500
5
6// unref returns .value for ref, or the value directly
7console.log(unref(temperature))      // 2500
8console.log(unref(plainReading)) // 1500
9
10// Useful in universal functions
11function convertToFahrenheit(kelvin) {
12  return (unref(kelvin) - 273.15) * 9/5 + 32
13}
14
15convertToFahrenheit(temperature)      // 4040.3299999999995 (about 4040.33)
16convertToFahrenheit(plainReading) // 2240.33

The long tail after the decimal point is ordinary JavaScript floating-point arithmetic, not Vue's fault - for display use toFixed(2). Since Vue 3.3 there is also toValue(), which works like unref() and additionally calls getters passed to it.

toRaw() - Raw Sensor Object

toRaw() returns the original object hidden behind the Proxy. It is an escape hatch: reads through it are not tracked, and writes do not refresh the view.

1import { reactive, toRaw } from 'vue'
2
3const reactor = reactive({
4  name: 'Plasma Core Alpha',
5  temperature: 2500
6})
7
8const rawReactor = toRaw(reactor)
9console.log(rawReactor) // Original object without Proxy
10
11// Useful for:
12// - Comparing objects
13// - Passing to APIs that do not accept a Proxy (e.g. structuredClone)
14// - External libraries

JSON.stringify() handles a Proxy without trouble, but structuredClone() or postMessage() throw a DataCloneError - give them toRaw(reactor) instead. The docs advise against keeping a persistent reference to the raw object, because it is easy to make changes the panel never notices.

markRaw() - Disabling Reactivity

markRaw() marks an object so that Vue never turns it into a Proxy, and returns the same object:

1import { reactive, markRaw } from 'vue'
2
3const externalSensorLibrary = markRaw({
4  hardwareInterface: new Map(),
5  calibrate() { /* ... */ }
6})
7
8const state = reactive({
9  sensor: externalSensorLibrary // Will not be reactive
10})
11
12// Useful for:
13// - Hardware libraries (sensor drivers, hardware interfaces)
14// - Large immutable data from the NOVA LAB database
15// - Vue component objects

isReactive(state.sensor) returns false, and state.sensor is exactly the same object as externalSensorLibrary. A Proxy can even break some library classes: a method that reaches for a private field written with a hash, for example #points, throws a TypeError in that case. markRaw prevents this. Note: the mark applies only to the object itself, not to its nested fields.

Debugging with watch

watch(source, callback) calls a function with the new and the old value after every change of the source, and the immediate: true option runs it right away. We will cover watchers in module 4, but they can already help with the investigation:

1import { ref, watch } from 'vue'
2
3const count = ref(0)
4
5watch(count, (newValue, oldValue) => {
6  console.log(`count changed: ${oldValue} β†’ ${newValue}`)
7  console.trace() // Stack trace - where did the change come from?
8}, { immediate: true })

The first entry is count changed: undefined β†’ 0, because at the start there is no old value yet. console.trace() prints the call stack, so you can see which function changed the reading. In development mode Vue also offers the onTrack and onTrigger options for watchers and computed properties, and the onRenderTracked and onRenderTriggered hooks.

Vue DevTools

Vue DevTools is a browser extension, also available as a Vite plugin. In Vue DevTools you can:

  • See all reactive data
  • Track changes in real time
  • Edit values live
  • See the dependency tree

The Components tab shows the component tree with its state, the Timeline records events and rendering times, and the Graph tab shows the reactive dependencies of the selected component. Install it on day one - it will save you hours of guessing. In the next lesson you will assemble all the tools you have learned into practical panels.

Remember: before you take the reactor apart, check it with a tester - isRef and isReactive tell you whether the wire is connected at all.

Code for this lesson: App.vue
1<script setup>
2import { ref, reactive, shallowRef, shallowReactive, readonly, triggerRef } from 'vue'
3
4// Advanced reactivity - shallow and readonly
5// NOVA LAB mission data protection system
6
7// shallowRef - reactivity only at .value level
8const missionData = shallowRef({
9  id: 'MISSION-2087',
10  crew: ['Chen', 'Rodriguez', 'Kim'],
11  objectives: {
12    primary: 'Establish Mars base',
13    secondary: 'Collect samples'
14  }
15})
16
17// shallowReactive - reactivity only at the first level
18const missionStatus = shallowReactive({
19  name: 'Mars Colony Alpha',
20  phase: 'DEPLOYMENT',
21  timeline: {
22    start: '2087-03-15',
23    current: '2087-06-20',
24    estimated_end: '2087-12-31'
25  },
26  modules: {
27    habitation: { status: 'ACTIVE', progress: 75 },
28    power: { status: 'ACTIVE', progress: 90 },
29    lifesupport: { status: 'TESTING', progress: 60 }
30  }
31})
32
33// readonly - read-only
34const criticalParameters = readonly(reactive({
35  maxCrewSize: 12,
36  minOxygen: 21,
37  maxRadiation: 100,
38  safetyProtocols: {
39    emergency: 'AUTO_SHUTDOWN',
40    backup: 'REDUNDANT_SYSTEMS'
41  }
42}))
43
44// Normal ref example for comparison
45const normalMissionData = ref({
46  id: 'MISSION-2087',
47  crew: ['Chen', 'Rodriguez', 'Kim']
48})
49
50// Demonstracja shallowRef
51function modifyShallowRef() {
52  // This will NOT work - change to a deeply nested property
53  missionData.value.objectives.primary = 'Build greenhouse'
54  console.log("Modified nested property, but UI won't update automatically")
55
56  // We must manually call triggerRef
57  // triggerRef(missionData)
58}
59
60function replaceShallowRef() {
61  // This WILL work - replacing the entire .value
62  missionData.value = {
63    ...missionData.value,
64    objectives: {
65      primary: 'Build greenhouse',
66      secondary: 'Test farming systems'
67    }
68  }
69}
70
71function forceUpdateShallowRef() {
72  // Modification of nested property
73  missionData.value.crew.push('Jackson')
74  // Manually forcing update
75  triggerRef(missionData)
76}
77
78// Demonstracja shallowReactive
79function modifyShallowReactive() {
80  // This WILL work - change at the first level
81  missionStatus.phase = 'OPERATIONAL'
82}
83
84function modifyDeepShallowReactive() {
85  // This will NOT work automatically - change deeper
86  missionStatus.modules.habitation.progress = 100
87  console.log("Modified deep property, UI won't update")
88}
89
90function replaceNestedShallowReactive() {
91  // This WILL work - replacing the object at the first level
92  missionStatus.modules = {
93    ...missionStatus.modules,
94    habitation: { status: 'COMPLETE', progress: 100 }
95  }
96}
97
98// Demonstracja readonly
99function tryModifyReadonly() {
100  try {
101    // This will NOT work - readonly blocks modifications
102    criticalParameters.maxCrewSize = 20
103  } catch (e) {
104    console.warn('Cannot modify readonly object')
105  }
106}
107
108// Counters for demonstration
109const shallowRefCounter = ref(0)
110const shallowReactiveCounter = ref(0)
111const readonlyCounter = ref(0)
112</script>
113
114<template>
115  <div class="advanced-reactivity">
116    <div class="header">
117      <h1>DATA PROTECTION SYSTEM - NOVA LAB</h1>
118      <p class="subtitle">shallowRef(), shallowReactive(), readonly(), triggerRef()</p>
119    </div>
120
121    <!-- shallowRef Demo -->
122    <div class="demo-section">
123      <h2>shallowRef() - Shallow reactivity</h2>
124      <div class="demo-grid">
125        <div class="info-card">
126          <h3>Characteristics</h3>
127          <ul>
128            <li>Reactive only at the <code>.value</code> level</li>
129            <li>Changes to deeply nested properties do NOT trigger updates</li>
130            <li>Use <code>triggerRef()</code> to force an update</li>
131            <li>More efficient for large objects</li>
132          </ul>
133        </div>
134
135        <div class="data-card">
136          <h3>Mission data (shallowRef)</h3>
137          <pre>{{ JSON.stringify(missionData, null, 2) }}</pre>
138
139          <div class="controls">
140            <button @click="modifyShallowRef(); shallowRefCounter++" class="btn btn-warning">
141              Modify nested (won't work)
142            </button>
143            <button @click="replaceShallowRef(); shallowRefCounter++" class="btn btn-success">
144              Replace .value (will work)
145            </button>
146            <button @click="forceUpdateShallowRef(); shallowRefCounter++" class="btn btn-info">
147              Force update (triggerRef)
148            </button>
149          </div>
150
151          <div class="counter">
152            Number of actions: {{ shallowRefCounter }}
153          </div>
154        </div>
155      </div>
156
157      <div class="code-example">
158        <h4>Code example:</h4>
159        <pre>const data = shallowRef({ nested: { value: 1 } })
160
161// Won't trigger UI update
162data.value.nested.value = 2
163
164// Will trigger UI update
165data.value = { nested: { value: 2 } }
166
167// Force update after modification
168data.value.nested.value = 2
169triggerRef(data)</pre>
170      </div>
171    </div>
172
173    <!-- shallowReactive Demo -->
174    <div class="demo-section">
175      <h2>shallowReactive() - Shallow reactivity for objects</h2>
176      <div class="demo-grid">
177        <div class="info-card">
178          <h3>Characteristics</h3>
179          <ul>
180            <li>Only first-level properties are reactive</li>
181            <li>Nested objects are NOT reactive</li>
182            <li>Nested objects must be replaced entirely</li>
183            <li>Efficient for flat structures</li>
184          </ul>
185        </div>
186
187        <div class="data-card">
188          <h3>Mission status (shallowReactive)</h3>
189          <div class="status-display">
190            <p><strong>Name:</strong> {{ missionStatus.name }}</p>
191            <p><strong>Phase:</strong>
192              <span class="phase-badge">{{ missionStatus.phase }}</span>
193            </p>
194            <p><strong>Timeline:</strong> {{ missionStatus.timeline.current }}</p>
195
196            <div class="modules-grid">
197              <div
198                v-for="(module, key) in missionStatus.modules"
199                :key="key"
200                class="module-card"
201              >
202                <div class="module-name">{{ key }}</div>
203                <div class="module-status">{{ module.status }}</div>
204                <div class="progress-bar">
205                  <div class="progress-fill" :style="{ width: module.progress + '%' }">
206                    {{ module.progress }}%
207                  </div>
208                </div>
209              </div>
210            </div>
211          </div>
212
213          <div class="controls">
214            <button @click="modifyShallowReactive(); shallowReactiveCounter++" class="btn btn-success">
215              Change phase (1st level)
216            </button>
217            <button @click="modifyDeepShallowReactive(); shallowReactiveCounter++" class="btn btn-warning">
218              Change progress (deep)
219            </button>
220            <button @click="replaceNestedShallowReactive(); shallowReactiveCounter++" class="btn btn-success">
221              Replace module
222            </button>
223          </div>
224
225          <div class="counter">
226            Number of actions: {{ shallowReactiveCounter }}
227          </div>
228        </div>
229      </div>
230
231      <div class="code-example">
232        <h4>Code example:</h4>
233        <pre>const state = shallowReactive({
234  count: 0,
235  nested: { value: 1 }
236})
237
238// Will work - first level
239state.count = 1
240
241// Won't work - nested
242state.nested.value = 2
243
244// Will work - replacement
245state.nested = { value: 2 }</pre>
246      </div>
247    </div>
248
249    <!-- readonly Demo -->
250    <div class="demo-section">
251      <h2>readonly() - Read-only data</h2>
252      <div class="demo-grid">
253        <div class="info-card">
254          <h3>Characteristics</h3>
255          <ul>
256            <li>Complete protection against modification</li>
257            <li>Ideal for constant configurations</li>
258            <li>Prevents accidental changes</li>
259            <li>Displays warnings in dev mode</li>
260          </ul>
261        </div>
262
263        <div class="data-card">
264          <h3>Critical parameters (readonly)</h3>
265          <div class="critical-params">
266            <div class="param-item">
267              <span class="param-label">Max crew:</span>
268              <span class="param-value">{{ criticalParameters.maxCrewSize }}</span>
269            </div>
270            <div class="param-item">
271              <span class="param-label">Min oxygen (%):</span>
272              <span class="param-value">{{ criticalParameters.minOxygen }}</span>
273            </div>
274            <div class="param-item">
275              <span class="param-label">Max radiation:</span>
276              <span class="param-value">{{ criticalParameters.maxRadiation }} mSv</span>
277            </div>
278            <div class="param-item">
279              <span class="param-label">Emergency protocol:</span>
280              <span class="param-value">{{ criticalParameters.safetyProtocols.emergency }}</span>
281            </div>
282            <div class="param-item">
283              <span class="param-label">System backup:</span>
284              <span class="param-value">{{ criticalParameters.safetyProtocols.backup }}</span>
285            </div>
286          </div>
287
288          <div class="controls">
289            <button @click="tryModifyReadonly(); readonlyCounter++" class="btn btn-danger">
290              Attempt modification (won't work)
291            </button>
292          </div>
293
294          <div class="counter">
295            Failed modification attempts: {{ readonlyCounter }}
296          </div>
297
298          <div class="warning-box">
299            Critical parameters are protected from modification
300          </div>
301        </div>
302      </div>
303
304      <div class="code-example">
305        <h4>Code example:</h4>
306        <pre>const config = readonly(reactive({
307  apiKey: 'secret',
308  maxRetries: 3
309}))
310
311// Won't work - readonly
312config.maxRetries = 5 // Warning in console
313
314// We can only read
315console.log(config.maxRetries) // 3</pre>
316      </div>
317    </div>
318
319    <div class="info-box">
320      <h3>When to use advanced functions?</h3>
321      <div class="use-cases">
322        <div class="use-case">
323          <h4>shallowRef()</h4>
324          <p>Large objects where you change the entire object at once, not individual properties</p>
325        </div>
326        <div class="use-case">
327          <h4>shallowReactive()</h4>
328          <p>Flat data structures with many properties at the first level</p>
329        </div>
330        <div class="use-case">
331          <h4>readonly()</h4>
332          <p>Configurations, constants, data that should not be modified</p>
333        </div>
334        <div class="use-case">
335          <h4>triggerRef()</h4>
336          <p>Manually forcing an update after modifying a shallowRef</p>
337        </div>
338      </div>
339    </div>
340  </div>
341</template>
342
343<style scoped>
344.advanced-reactivity {
345  min-height: 100vh;
346  background: #0a0e27;
347  color: #e0e0e0;
348  padding: 2rem;
349  font-family: 'Courier New', monospace;
350}
351
352.header {
353  text-align: center;
354  margin-bottom: 2rem;
355  border-bottom: 2px solid #00ff88;
356  padding-bottom: 1rem;
357}
358
359.header h1 {
360  color: #00ff88;
361  font-size: 2rem;
362  margin: 0;
363  text-shadow: 0 0 10px #00ff88;
364}
365
366.subtitle {
367  color: #00b4d8;
368  margin: 0.5rem 0 0 0;
369}
370
371.demo-section {
372  background: #1a1f3a;
373  padding: 1.5rem;
374  border-radius: 8px;
375  border: 1px solid #00b4d8;
376  margin-bottom: 2rem;
377}
378
379.demo-section h2 {
380  color: #00ff88;
381  margin-top: 0;
382}
383
384.demo-grid {
385  display: grid;
386  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
387  gap: 1.5rem;
388  margin-bottom: 1.5rem;
389}
390
391.info-card, .data-card {
392  background: #0a0e27;
393  padding: 1.5rem;
394  border-radius: 6px;
395}
396
397.info-card h3, .data-card h3 {
398  color: #00b4d8;
399  margin-top: 0;
400}
401
402.info-card ul {
403  list-style: none;
404  padding: 0;
405}
406
407.info-card li {
408  margin: 0.8rem 0;
409  padding-left: 1.5rem;
410  position: relative;
411}
412
413.info-card code {
414  background: #1a1f3a;
415  padding: 0.2rem 0.5rem;
416  border-radius: 3px;
417  color: #00ff88;
418}
419
420.data-card pre {
421  background: #1a1f3a;
422  padding: 1rem;
423  border-radius: 4px;
424  overflow-x: auto;
425  font-size: 0.85rem;
426  color: #00ff88;
427}
428
429.controls {
430  display: flex;
431  flex-direction: column;
432  gap: 0.8rem;
433  margin: 1rem 0;
434}
435
436.btn {
437  padding: 0.8rem 1rem;
438  font-size: 0.9rem;
439  border: none;
440  border-radius: 6px;
441  cursor: pointer;
442  font-weight: bold;
443  transition: all 0.3s ease;
444  font-family: 'Courier New', monospace;
445  text-align: left;
446}
447
448.btn-success {
449  background: linear-gradient(135deg, #00ff88, #00dd77);
450  color: #0a0e27;
451}
452
453.btn-warning {
454  background: linear-gradient(135deg, #ffaa00, #ff8800);
455  color: #0a0e27;
456}
457
458.btn-danger {
459  background: linear-gradient(135deg, #ff4444, #cc0000);
460  color: white;
461}
462
463.btn-info {
464  background: linear-gradient(135deg, #00b4d8, #0088cc);
465  color: white;
466}
467
468.btn:hover {
469  transform: scale(1.02);
470  box-shadow: 0 0 15px currentColor;
471}
472
473.counter {
474  background: #1a1f3a;
475  padding: 0.8rem;
476  border-radius: 4px;
477  text-align: center;
478  color: #00ff88;
479  font-weight: bold;
480  margin-top: 1rem;
481}
482
483.status-display p {
484  margin: 0.5rem 0;
485  color: #ccc;
486}
487
488.phase-badge {
489  background: #00ff88;
490  color: #0a0e27;
491  padding: 0.3rem 0.8rem;
492  border-radius: 4px;
493  font-weight: bold;
494}
495
496.modules-grid {
497  display: grid;
498  gap: 1rem;
499  margin-top: 1rem;
500}
501
502.module-card {
503  background: #1a1f3a;
504  padding: 1rem;
505  border-radius: 4px;
506}
507
508.module-name {
509  color: #00b4d8;
510  font-weight: bold;
511  text-transform: uppercase;
512}
513
514.module-status {
515  color: #00ff88;
516  font-size: 0.9rem;
517  margin: 0.3rem 0;
518}
519
520.progress-bar {
521  height: 24px;
522  background: #0a0e27;
523  border-radius: 4px;
524  overflow: hidden;
525  margin-top: 0.5rem;
526}
527
528.progress-fill {
529  height: 100%;
530  background: linear-gradient(90deg, #00b4d8, #00ff88);
531  display: flex;
532  align-items: center;
533  justify-content: center;
534  color: #0a0e27;
535  font-weight: bold;
536  transition: width 0.3s ease;
537}
538
539.critical-params {
540  background: #1a1f3a;
541  padding: 1rem;
542  border-radius: 4px;
543  border-left: 3px solid #ff4444;
544}
545
546.param-item {
547  display: flex;
548  justify-content: space-between;
549  padding: 0.5rem 0;
550  border-bottom: 1px solid #333;
551}
552
553.param-item:last-child {
554  border-bottom: none;
555}
556
557.param-label {
558  color: #888;
559}
560
561.param-value {
562  color: #00ff88;
563  font-weight: bold;
564}
565
566.warning-box {
567  background: #2a1f1a;
568  border: 1px solid #ff8800;
569  padding: 1rem;
570  border-radius: 4px;
571  color: #ffaa00;
572  margin-top: 1rem;
573  text-align: center;
574}
575
576.code-example {
577  background: #0a0e27;
578  padding: 1rem;
579  border-radius: 6px;
580  border-left: 3px solid #00ff88;
581}
582
583.code-example h4 {
584  color: #00b4d8;
585  margin-top: 0;
586}
587
588.code-example pre {
589  color: #00ff88;
590  font-size: 0.85rem;
591  overflow-x: auto;
592  margin: 0;
593}
594
595.info-box {
596  background: #1a1f3a;
597  padding: 1.5rem;
598  border-radius: 8px;
599  border-left: 4px solid #00ff88;
600}
601
602.info-box h3 {
603  color: #00ff88;
604  margin-top: 0;
605}
606
607.use-cases {
608  display: grid;
609  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
610  gap: 1rem;
611  margin-top: 1rem;
612}
613
614.use-case {
615  background: #0a0e27;
616  padding: 1rem;
617  border-radius: 4px;
618}
619
620.use-case h4 {
621  color: #00b4d8;
622  margin-top: 0;
623}
624
625.use-case p {
626  color: #ccc;
627  margin: 0;
628  font-size: 0.9rem;
629}
630</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 does the isRef() function return?

  2. 2. What does unref(value) do?

These are 2 of 5 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create ref and reactive variables and use isRef() and isReactive() to check their types.

  • Click in order

    Arrange the code that checks the type of a value:

  • Code editor

    Write a function that uses unref() to accept both ref and regular values as arguments.

Useful articles