Vue.js course Β· Module 3: Reactivity

Deep vs Shallow Reactivity

5 min read
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 update

Each 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 update

After 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:

SituationUse
Large objects from APIshallowRef
External librariesshallowRef
Performance optimizationshallow*
Typical UI dataref/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. 1. How deep is the default reactivity of ref() and reactive()?

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

Useful articles