Vue.js course Β· Module 3: Reactivity
Debugging Reactivity
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)) // falseA 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.33The 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 librariesJSON.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 objectsisReactive(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. What does the isRef() function return?
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.