Vue.js course Β· Module 3: Reactivity
Deep vs Shallow Reactivity
In this lesson5
Vue offers different levels of reactivity - like different layers of sensors in a reactor.
Every few seconds the reactor telemetry sends a batch of thousands of readings. You replace the whole batch every time, and yet default reactivity wraps every nested object you touch in a Proxy and tracks every reading separately. It is like mounting a sensor on every bolt of the cargo when all you care about is whether a new crate has arrived. For such situations Vue has shallow versions.
Deep Reactivity (default)
By default reactivity is deep: ref() holding an object and reactive() track changes at every level of nesting, arrays included.
1import { ref, reactive } from 'vue'
2
3// ref with object - deep reactivity
4const missionControl = ref({
5 station: 'NOVA LAB',
6 modules: [
7 { id: 1, name: 'Oxygen Generator' },
8 { id: 2, name: 'Water Recycler' }
9 ]
10})
11
12// Changes at any level are reactive
13missionControl.value.station = 'NOVA LAB Alpha'
14missionControl.value.modules[0].name = 'Oxygen Generator Prime'
15missionControl.value.modules.push({ id: 3, name: 'Power Core' })
16// All of the above will trigger a control panel updateEach of the three changes refreshes the panel, because ref() passed the object to reactive(), which wraps nested objects as soon as you reach for them. For typical interface data this is convenient and fast enough.
shallowRef() - Shallow Reactivity
shallowRef() stores the value without turning it into a Proxy. Only access to .value is reactive, which means replacing the whole content:
1import { shallowRef } from 'vue'
2
3const missionControl = shallowRef({
4 station: 'NOVA LAB',
5 modules: [{ id: 1, name: 'Oxygen Generator' }]
6})
7
8// Only .value replacement is reactive
9missionControl.value.station = 'New name' // Will NOT trigger update!
10missionControl.value.modules.push({ id: 2, name: 'Water Recycler' }) // Will NOT trigger update!
11
12// THIS will work - replacing the entire object
13missionControl.value = {
14 station: 'NOVA LAB Beta',
15 modules: []
16}The first two changes really are saved in memory - only the screen does not change, because Vue does not know about them. They will show up at the next render triggered by something else. To change one field in a shallowRef called sensorData, build a new object with the spread operator:
1sensorData.value = {
2 ...sensorData.value,
3 temperature: 3000
4}The new object goes through the .value setter, so the view refreshes, and the old data batch stays untouched. Copying a huge array just to add one element can be wasteful, though. In that case you change the content in place and call triggerRef(), which manually runs everything that depends on the shallow ref:
1import { shallowRef, triggerRef } from 'vue'
2
3const readings = shallowRef([])
4
5readings.value.push({ id: 1, temperature: 2500 }) // the view does not know yet
6triggerRef(readings) // force an updateAfter triggerRef the view refreshes once. The responsibility for that call is yours - forget it, and the panel will show old data.
shallowReactive()
shallowReactive() makes only the first-level properties reactive. Nested objects are stored as they are, so Vue cannot see inside them:
1import { shallowReactive } from 'vue'
2
3const missionControl = shallowReactive({
4 station: 'NOVA LAB',
5 location: {
6 planet: 'Mars',
7 year: 2087
8 }
9})
10
11missionControl.station = 'New station' // Reactive - level 1
12missionControl.location.planet = 'Europa' // NOT reactive - level 2
13
14// You must replace the entire location object
15missionControl.location = { planet: 'Europa', year: 2087 }Replacing location is a change to a first-level property, which is why it works. The docs warn: use shallow structures only as the root state of a component, and do not nest them inside deeply reactive objects, because that creates a tree with inconsistent behavior.
When to use shallow?
Shallow versions are an optimization tool, not a new default choice:
| Situation | Use |
|---|---|
| Large objects from API | shallowRef |
| External libraries | shallowRef |
| Performance optimization | shallow* |
| Typical UI data | ref/reactive |
My advice: start with ref and reactive, and reach for the shallow versions for large, immutable data or state managed by an external library.
Practical Example
The monitoring panel fetches sensor data and always replaces the whole batch. The template shows the system name and a refresh button:
1<template>
2 <div class="sensor-display" style="background: #0a0e27; color: #00ff88;">
3 <h2>{{ sensorData.systemName }}</h2>
4 <button @click="refreshData">Refresh readings</button>
5 </div>
6</template>The script keeps the data in a shallowRef, and an asynchronous function fetches new data with fetch, the browser API for network requests:
1<script setup>
2import { shallowRef } from 'vue'
3
4// Large sensor data - shallow reactivity
5const sensorData = shallowRef({
6 systemName: 'Reactor Monitoring',
7 readings: [] // Thousands of sensor readings
8})
9
10async function refreshData() {
11 const response = await fetch('/api/sensors')
12 // Replacing the entire object - reactive
13 sensorData.value = await response.json()
14}
15</script>await waits for the response, and then the assignment to .value refreshes the panel. Vue does not wrap thousands of readings, because nobody modifies them one by one. In the next lesson you will pull single properties out of a reactive object without breaking the connection.
Remember: deep reactivity is a sensor on every bolt, and shallow reactivity is one sensor on the hatch - it notices only a swap of the whole cargo or a manual triggerRef signal.
Code for this lesson: App.vue
1<script setup>
2import { reactive, toRef, toRefs } from 'vue'
3
4// toRef() and toRefs() - Extracting reactive properties
5// NOVA LAB crew member management system
6
7const crewMember = reactive({
8 id: 'CREW-001',
9 name: 'Dr. Sarah Chen',
10 role: 'Chief Engineer',
11 status: 'ACTIVE',
12 stats: {
13 health: 95,
14 energy: 80,
15 morale: 90
16 },
17 certifications: ['Reactor Operations', 'EVA Specialist', 'Life Support'],
18 missions: 12
19})
20
21// toRef() - creates a ref to a single property
22// Remains reactive and synchronized with the original object
23const memberName = toRef(crewMember, 'name')
24const memberStatus = toRef(crewMember, 'status')
25const memberHealth = toRef(crewMember.stats, 'health')
26
27// toRefs() - creates refs to all properties of an object
28// Useful for destructuring
29const { role, missions } = toRefs(crewMember)
30
31// Second crew member for demonstration
32const secondCrew = reactive({
33 id: 'CREW-002',
34 name: 'Cpt. Marcus Rodriguez',
35 role: 'Mission Commander',
36 status: 'ACTIVE',
37 stats: {
38 health: 88,
39 energy: 75,
40 morale: 85
41 },
42 certifications: ['Command', 'Navigation', 'Emergency Response'],
43 missions: 25
44})
45
46// We use toRefs() for destructuring
47const {
48 name: secondName,
49 role: secondRole,
50 missions: secondMissions,
51 status: secondStatus
52} = toRefs(secondCrew)
53
54// Modification functions
55function updateStatus(newStatus) {
56 // Modification via toRef - changes the original object
57 memberStatus.value = newStatus
58}
59
60function decreaseHealth() {
61 // Modification via toRef - affects the original object
62 if (memberHealth.value > 0) {
63 memberHealth.value -= 10
64 }
65}
66
67function increaseMissions() {
68 // Modification via toRefs - affects the original object
69 missions.value++
70}
71
72function changeRole(newRole) {
73 // Modification via toRefs - affects the original object
74 role.value = newRole
75}
76
77function promoteSecondCrew() {
78 secondMissions.value++
79 if (secondMissions.value > 30) {
80 secondRole.value = 'Senior Commander'
81 secondStatus.value = 'VETERAN'
82 }
83}
84</script>
85
86<template>
87 <div class="crew-management">
88 <div class="header">
89 <h1>CREW MANAGEMENT - NOVA LAB</h1>
90 <p class="subtitle">toRef() and toRefs() - Extracting reactive properties</p>
91 </div>
92
93 <div class="crew-grid">
94 <!-- First crew member - toRef() -->
95 <div class="crew-card">
96 <div class="crew-header">
97 <h2>{{ crewMember.name }}</h2>
98 <span :class="'status-badge status-' + crewMember.status.toLowerCase()">
99 {{ crewMember.status }}
100 </span>
101 </div>
102
103 <div class="crew-info">
104 <p><strong>ID:</strong> {{ crewMember.id }}</p>
105 <p><strong>Role:</strong> {{ crewMember.role }}</p>
106 <p><strong>Missions:</strong> {{ crewMember.missions }}</p>
107 </div>
108
109 <div class="stats-section">
110 <h3>Statistics</h3>
111 <div class="stat-bar">
112 <label>Health</label>
113 <div class="progress">
114 <div class="progress-fill health" :style="{ width: crewMember.stats.health + '%' }">
115 {{ crewMember.stats.health }}%
116 </div>
117 </div>
118 </div>
119 <div class="stat-bar">
120 <label>Energy</label>
121 <div class="progress">
122 <div class="progress-fill energy" :style="{ width: crewMember.stats.energy + '%' }">
123 {{ crewMember.stats.energy }}%
124 </div>
125 </div>
126 </div>
127 <div class="stat-bar">
128 <label>Morale</label>
129 <div class="progress">
130 <div class="progress-fill morale" :style="{ width: crewMember.stats.morale + '%' }">
131 {{ crewMember.stats.morale }}%
132 </div>
133 </div>
134 </div>
135 </div>
136
137 <div class="certifications">
138 <h3>Certifications</h3>
139 <div class="cert-tags">
140 <span v-for="cert in crewMember.certifications" :key="cert" class="cert-tag">
141 {{ cert }}
142 </span>
143 </div>
144 </div>
145
146 <div class="controls">
147 <button @click="updateStatus('ON_MISSION')" class="btn btn-small">
148 Send on mission
149 </button>
150 <button @click="updateStatus('RESTING')" class="btn btn-small">
151 Rest
152 </button>
153 <button @click="decreaseHealth()" class="btn btn-small btn-warning">
154 Simulate injury
155 </button>
156 <button @click="increaseMissions()" class="btn btn-small btn-success">
157 Add mission
158 </button>
159 </div>
160
161 <div class="code-box">
162 <pre>// toRef() - single property
163const name = toRef(crewMember, 'name')
164const health = toRef(crewMember.stats, 'health')
165
166// Modification
167health.value -= 10</pre>
168 </div>
169 </div>
170
171 <!-- Second crew member - toRefs() -->
172 <div class="crew-card">
173 <div class="crew-header">
174 <h2>{{ secondCrew.name }}</h2>
175 <span :class="'status-badge status-' + secondCrew.status.toLowerCase()">
176 {{ secondCrew.status }}
177 </span>
178 </div>
179
180 <div class="crew-info">
181 <p><strong>ID:</strong> {{ secondCrew.id }}</p>
182 <p><strong>Role:</strong> {{ secondRole }}</p>
183 <p><strong>Missions:</strong> {{ secondMissions }}</p>
184 </div>
185
186 <div class="stats-section">
187 <h3>Statistics</h3>
188 <div class="stat-bar">
189 <label>Health</label>
190 <div class="progress">
191 <div class="progress-fill health" :style="{ width: secondCrew.stats.health + '%' }">
192 {{ secondCrew.stats.health }}%
193 </div>
194 </div>
195 </div>
196 <div class="stat-bar">
197 <label>Energy</label>
198 <div class="progress">
199 <div class="progress-fill energy" :style="{ width: secondCrew.stats.energy + '%' }">
200 {{ secondCrew.stats.energy }}%
201 </div>
202 </div>
203 </div>
204 <div class="stat-bar">
205 <label>Morale</label>
206 <div class="progress">
207 <div class="progress-fill morale" :style="{ width: secondCrew.stats.morale + '%' }">
208 {{ secondCrew.stats.morale }}%
209 </div>
210 </div>
211 </div>
212 </div>
213
214 <div class="certifications">
215 <h3>Certifications</h3>
216 <div class="cert-tags">
217 <span v-for="cert in secondCrew.certifications" :key="cert" class="cert-tag">
218 {{ cert }}
219 </span>
220 </div>
221 </div>
222
223 <div class="controls">
224 <button @click="promoteSecondCrew()" class="btn btn-small btn-success">
225 Promote
226 </button>
227 <button @click="changeRole('Flight Director')" class="btn btn-small">
228 Change role
229 </button>
230 <button @click="secondStatus.value = 'ACTIVE'" class="btn btn-small">
231 Activate
232 </button>
233 </div>
234
235 <div class="code-box">
236 <pre>// toRefs() - all properties
237const { role, missions, status } = toRefs(secondCrew)
238
239// Modification
240missions.value++
241role.value = 'Senior Commander'</pre>
242 </div>
243 </div>
244 </div>
245
246 <div class="info-box">
247 <h3>Difference between toRef() and toRefs()</h3>
248 <div class="comparison-grid">
249 <div class="comparison-item">
250 <h4>toRef(object, 'key')</h4>
251 <ul>
252 <li>Creates a ref to a <strong>single</strong> property</li>
253 <li>Useful when we only need one value</li>
254 <li>Maintains reactivity with the original object</li>
255 <li>Example: <code>const name = toRef(user, 'name')</code></li>
256 </ul>
257 </div>
258 <div class="comparison-item">
259 <h4>toRefs(object)</h4>
260 <ul>
261 <li>Creates refs to <strong>all</strong> properties</li>
262 <li>Useful for destructuring</li>
263 <li>Each property becomes a ref</li>
264 <li>Example: <code>const { name, age } = toRefs(user)</code></li>
265 </ul>
266 </div>
267 </div>
268 <div class="warning-box">
269 <p><strong>Important:</strong> Regular destructuring will lose reactivity!</p>
270 <code>const { name } = crewMember // Not reactive</code><br>
271 <code>const { name } = toRefs(crewMember) // Reactive</code>
272 </div>
273 </div>
274 </div>
275</template>
276
277<style scoped>
278.crew-management {
279 min-height: 100vh;
280 background: #0a0e27;
281 color: #e0e0e0;
282 padding: 2rem;
283 font-family: 'Courier New', monospace;
284}
285
286.header {
287 text-align: center;
288 margin-bottom: 2rem;
289 border-bottom: 2px solid #00ff88;
290 padding-bottom: 1rem;
291}
292
293.header h1 {
294 color: #00ff88;
295 font-size: 2rem;
296 margin: 0;
297 text-shadow: 0 0 10px #00ff88;
298}
299
300.subtitle {
301 color: #00b4d8;
302 margin: 0.5rem 0 0 0;
303}
304
305.crew-grid {
306 display: grid;
307 grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
308 gap: 2rem;
309 margin-bottom: 2rem;
310}
311
312.crew-card {
313 background: #1a1f3a;
314 padding: 1.5rem;
315 border-radius: 8px;
316 border: 1px solid #00b4d8;
317}
318
319.crew-header {
320 display: flex;
321 justify-content: space-between;
322 align-items: center;
323 margin-bottom: 1rem;
324 padding-bottom: 1rem;
325 border-bottom: 1px solid #333;
326}
327
328.crew-header h2 {
329 color: #00ff88;
330 margin: 0;
331 font-size: 1.3rem;
332}
333
334.status-badge {
335 padding: 0.3rem 0.8rem;
336 border-radius: 20px;
337 font-size: 0.8rem;
338 font-weight: bold;
339}
340
341.status-active {
342 background: #00ff88;
343 color: #0a0e27;
344}
345
346.status-on_mission {
347 background: #00b4d8;
348 color: #0a0e27;
349}
350
351.status-resting {
352 background: #ffaa00;
353 color: #0a0e27;
354}
355
356.status-veteran {
357 background: linear-gradient(135deg, #ffd700, #ffaa00);
358 color: #0a0e27;
359}
360
361.crew-info p {
362 margin: 0.5rem 0;
363 color: #ccc;
364}
365
366.stats-section {
367 margin: 1.5rem 0;
368}
369
370.stats-section h3 {
371 color: #00b4d8;
372 font-size: 1rem;
373 margin-bottom: 1rem;
374}
375
376.stat-bar {
377 margin-bottom: 1rem;
378}
379
380.stat-bar label {
381 display: block;
382 font-size: 0.9rem;
383 color: #888;
384 margin-bottom: 0.3rem;
385}
386
387.progress {
388 height: 24px;
389 background: #0a0e27;
390 border-radius: 4px;
391 overflow: hidden;
392}
393
394.progress-fill {
395 height: 100%;
396 display: flex;
397 align-items: center;
398 justify-content: center;
399 color: #0a0e27;
400 font-weight: bold;
401 font-size: 0.85rem;
402 transition: width 0.3s ease;
403}
404
405.progress-fill.health {
406 background: linear-gradient(90deg, #ff4444, #ff6666);
407}
408
409.progress-fill.energy {
410 background: linear-gradient(90deg, #00b4d8, #00d4ff);
411}
412
413.progress-fill.morale {
414 background: linear-gradient(90deg, #00ff88, #44ffaa);
415}
416
417.certifications {
418 margin: 1.5rem 0;
419}
420
421.certifications h3 {
422 color: #00b4d8;
423 font-size: 1rem;
424 margin-bottom: 0.8rem;
425}
426
427.cert-tags {
428 display: flex;
429 flex-wrap: wrap;
430 gap: 0.5rem;
431}
432
433.cert-tag {
434 background: #0a0e27;
435 padding: 0.4rem 0.8rem;
436 border-radius: 4px;
437 border: 1px solid #00ff88;
438 color: #00ff88;
439 font-size: 0.85rem;
440}
441
442.controls {
443 display: flex;
444 flex-wrap: wrap;
445 gap: 0.5rem;
446 margin: 1rem 0;
447}
448
449.btn {
450 padding: 0.6rem 1rem;
451 font-size: 0.9rem;
452 border: none;
453 border-radius: 6px;
454 cursor: pointer;
455 font-weight: bold;
456 transition: all 0.3s ease;
457 font-family: 'Courier New', monospace;
458 background: #00b4d8;
459 color: #0a0e27;
460}
461
462.btn-small {
463 font-size: 0.85rem;
464 padding: 0.5rem 0.8rem;
465}
466
467.btn-success {
468 background: linear-gradient(135deg, #00ff88, #00dd77);
469}
470
471.btn-warning {
472 background: linear-gradient(135deg, #ff6666, #ff4444);
473 color: white;
474}
475
476.btn:hover {
477 transform: scale(1.05);
478 box-shadow: 0 0 15px currentColor;
479}
480
481.code-box {
482 background: #0a0e27;
483 padding: 1rem;
484 border-radius: 4px;
485 border-left: 3px solid #00ff88;
486 margin-top: 1rem;
487}
488
489.code-box pre {
490 margin: 0;
491 color: #00ff88;
492 font-size: 0.85rem;
493 overflow-x: auto;
494}
495
496.info-box {
497 background: #1a1f3a;
498 padding: 1.5rem;
499 border-radius: 8px;
500 border-left: 4px solid #00ff88;
501}
502
503.info-box h3 {
504 color: #00ff88;
505 margin-top: 0;
506}
507
508.comparison-grid {
509 display: grid;
510 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
511 gap: 1.5rem;
512 margin: 1rem 0;
513}
514
515.comparison-item h4 {
516 color: #00b4d8;
517 margin-top: 0;
518}
519
520.comparison-item ul {
521 padding-left: 1.5rem;
522 color: #ccc;
523}
524
525.comparison-item li {
526 margin: 0.5rem 0;
527}
528
529.comparison-item code {
530 background: #0a0e27;
531 padding: 0.2rem 0.5rem;
532 border-radius: 3px;
533 color: #00ff88;
534}
535
536.warning-box {
537 background: #2a1f1a;
538 padding: 1rem;
539 border-radius: 4px;
540 border-left: 3px solid #ffaa00;
541 margin-top: 1rem;
542}
543
544.warning-box code {
545 background: #0a0e27;
546 padding: 0.3rem 0.6rem;
547 border-radius: 3px;
548 color: #00ff88;
549 display: inline-block;
550 margin: 0.2rem 0;
551}
552</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. How deep is the default reactivity of ref() and reactive()?
2. How does shallowRef() differ from ref()?
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 shallowRef with an object and demonstrate the difference - modifying a nested property won't trigger a re-render.
- Horizontal ordering
Arrange the elements for correctly updating a shallowRef in the correct order.
- Click in order
Arrange the reactive functions from deepest to shallowest reactivity.
- Code editor
Create a shallowReactive object and show the difference between modifying first-level and nested properties.