Vue.js course · Module 3: Reactivity

reactive() - Reactive Systems

5 min read
In this lesson4

When working with complex system objects, reactive() is often more convenient than ref() - it's like a complete reactor control panel instead of individual gauges.

The reactor has a name, a status, a temperature and two readings: power and radiation. Five separate refs mean five declarations and .value on every line of the script, even though all this data describes a single device. reactive() lets you enclose it in one object that is reactive as a whole.

Basics of reactive()

reactive() takes an object and returns its Proxy - a mechanism built into JavaScript that intercepts every read and write of a property. Thanks to that you do not need .value, and nested objects become reactive too:

1import { reactive } from 'vue'
2
3const reactor = reactive({
4  name: 'Plasma Core Alpha',
5  status: 'ACTIVE',
6  temperature: 2500,
7  readings: {
8    power: 850,
9    radiation: 12
10  }
11})
12
13// Access without .value!
14console.log(reactor.name) // 'Plasma Core Alpha'
15reactor.temperature = 2600
16
17// Nested objects are also reactive
18reactor.readings.power = 900 // This will also trigger a panel update

The Proxy acts like a dispatcher between you and the data: it reports every change to the system, which refreshes the dependent views. Notice what did not change - only the object returned by reactive() is reactive, not the original you passed in:

1const raw = { status: 'STANDBY' }
2const panel = reactive(raw)
3
4console.log(panel === raw)           // false
5console.log(reactive(raw) === panel) // true

The Proxy is a different object from the original, and changes written directly to raw will not refresh the view. Calling reactive() again on the same object returns the same Proxy, so always work with the reactive version.

reactive() vs ref()

Both tools give you reactivity, but they differ in everyday use:

Featureref()reactive()
For primitivesYesNo
For objectsYes (.value)Yes (directly)
DestructuringSafe (you pass the whole container)Loses reactivity
ReassignYes (.value = newObj)No (only properties)

You can pass a ref to a function or take it out of an object and it stays reactive, because the whole container travels with it. From reactive(), on the other hand, you pull out bare values. The Vue docs recommend ref() as the primary API precisely because of these limitations.

Practical Example

Here is a reactor panel built on a single object. The template also reads the nested readings:

1<template>
2  <div class="reactor-panel" style="background: #0a0e27; color: #00ff88; border: 1px solid #00b4d8;">
3    <h2>{{ reactor.name }}</h2>
4    <p>Status: {{ reactor.status }}</p>
5    <p>Temperature: {{ reactor.temperature }} K</p>
6    <p>Readings: {{ reactor.readings.power }} MW | {{ reactor.readings.radiation }} Sv/h</p>
7
8    <button @click="updateTemperature">Increase temperature</button>
9  </div>
10</template>

There is not a single .value in the template, because reactor is a reactive object, not a ref. The script creates this object and a function that changes one property:

1<script setup>
2import { reactive } from 'vue'
3
4const reactor = reactive({
5  name: 'Plasma Core Alpha',
6  status: 'ACTIVE',
7  temperature: 2500,
8  readings: {
9    power: 850,
10    radiation: 12
11  }
12})
13
14function updateTemperature() {
15  reactor.temperature += 100 // Direct modification
16}
17</script>

Each click raises the temperature by 100 K, and the panel shows the new value. The reactor variable points to the same Proxy the whole time - only its properties change.

Limitations of reactive()

The first limitation: reactive() works only with objects, arrays and the Map and Set collections. For a number, Vue prints a warning and returns it unchanged. The second one concerns replacing the whole object:

1import { reactive } from 'vue'
2
3const reactor = reactive({ name: 'Plasma Core Alpha' })
4
5// BAD - replacing the entire object
6reactor = { name: 'Plasma Core Beta' } // This won't work!
7
8// GOOD - modifying properties
9reactor.name = 'Plasma Core Beta'

With const, JavaScript itself throws a TypeError (assignment to a constant). With let, the variable would point to a new, plain object while the template stayed with the old Proxy, so the connection would break. That is why you change properties, not the whole object. When new data arrives in one piece, for example from telemetry, copy it over with Object.assign(reactor, newData) - that is still a change to the properties of the same Proxy. The third limitation is destructuring:

1// BAD - destructuring loses reactivity
2const { name } = reactor // name is not reactive!
3
4// GOOD - use toRefs
5import { toRefs } from 'vue'
6const { name } = toRefs(reactor) // name.value is reactive

Destructuring copies the current value, while toRefs creates refs linked to the properties - we will cover it in detail in the lesson about toRef and toRefs. My practice at NOVA LAB: ref() by default, and reactive() for a coherent group of fields, such as a configuration form. In the next lesson you will check how deep this reactivity goes and when it is worth making it shallow.

Remember: reactive() is a control panel behind Proxy glass - you turn the knobs, but you never swap out the whole panel.

Code for this lesson: App.vue
1<script setup>
2import { ref, reactive } from 'vue'
3
4// Comparison ref() vs reactive()
5// Managing oxygen tanks at NOVA LAB station
6
7// ref() - individual values
8const mainTankLevel = ref(100)
9const backupTankLevel = ref(75)
10const oxygenFlow = ref(2.5)
11
12// reactive() - complex object
13const oxygenSystem = reactive({
14  mainTank: {
15    level: 100,
16    capacity: 1000,
17    pressure: 150,
18    status: 'OPTIMAL'
19  },
20  backupTank: {
21    level: 75,
22    capacity: 500,
23    pressure: 120,
24    status: 'STANDBY'
25  },
26  flow: {
27    rate: 2.5,
28    direction: 'OUT',
29    consumers: 12
30  }
31})
32
33// Oxygen consumption simulation
34let simulationInterval = null
35
36function startSimulation() {
37  if (simulationInterval) return
38
39  simulationInterval = setInterval(() => {
40    // Using ref - we must use .value
41    if (mainTankLevel.value > 0) {
42      mainTankLevel.value -= 0.5
43    }
44
45    // Using reactive - direct property access
46    if (oxygenSystem.mainTank.level > 0) {
47      oxygenSystem.mainTank.level -= 0.5
48
49      if (oxygenSystem.mainTank.level < 30) {
50        oxygenSystem.mainTank.status = 'WARNING'
51      }
52      if (oxygenSystem.mainTank.level < 10) {
53        oxygenSystem.mainTank.status = 'CRITICAL'
54      }
55    }
56  }, 100)
57}
58
59function stopSimulation() {
60  if (simulationInterval) {
61    clearInterval(simulationInterval)
62    simulationInterval = null
63  }
64}
65
66function refill() {
67  // ref - we use .value
68  mainTankLevel.value = 100
69  backupTankLevel.value = 75
70
71  // reactive - direct access
72  oxygenSystem.mainTank.level = 100
73  oxygenSystem.mainTank.status = 'OPTIMAL'
74  oxygenSystem.backupTank.level = 75
75}
76
77// Example: we cannot replace the entire reactive object
78function wrongWayToUpdate() {
79  // THIS WILL NOT WORK - will lose reactivity
80  // oxygenSystem = { ...newData }
81
82  // CORRECT WAY - update properties
83  oxygenSystem.mainTank.level = 100
84  oxygenSystem.mainTank.status = 'OPTIMAL'
85}
86</script>
87
88<template>
89  <div class="oxygen-control">
90    <div class="header">
91      <h1>OXYGEN SYSTEM - NOVA LAB</h1>
92      <p class="subtitle">ref() vs reactive() - Comparison</p>
93    </div>
94
95    <div class="comparison-grid">
96      <!-- ref() section -->
97      <div class="section">
98        <h2 class="section-title">USING ref()</h2>
99        <div class="card">
100          <div class="tank-visual">
101            <div class="tank-fill" :style="{ height: mainTankLevel + '%' }">
102              <span class="tank-label">{{ mainTankLevel.toFixed(1) }}%</span>
103            </div>
104          </div>
105          <div class="tank-info">
106            <p><strong>Main tank:</strong> {{ mainTankLevel.toFixed(1) }}%</p>
107            <p><strong>Backup tank:</strong> {{ backupTankLevel.toFixed(1) }}%</p>
108            <p><strong>Flow:</strong> {{ oxygenFlow }} L/min</p>
109          </div>
110          <div class="code-example">
111            <pre>const level = ref(100)
112// Access: level.value
113level.value = 50</pre>
114          </div>
115        </div>
116      </div>
117
118      <!-- reactive() section -->
119      <div class="section">
120        <h2 class="section-title">USING reactive()</h2>
121        <div class="card">
122          <div class="tank-visual">
123            <div
124              class="tank-fill"
125              :style="{ height: oxygenSystem.mainTank.level + '%' }"
126              :class="oxygenSystem.mainTank.status.toLowerCase()"
127            >
128              <span class="tank-label">{{ oxygenSystem.mainTank.level.toFixed(1) }}%</span>
129            </div>
130          </div>
131          <div class="tank-info">
132            <p>
133              <strong>Status:</strong>
134              <span :class="'status-' + oxygenSystem.mainTank.status.toLowerCase()">
135                {{ oxygenSystem.mainTank.status }}
136              </span>
137            </p>
138            <p><strong>Capacity:</strong> {{ oxygenSystem.mainTank.capacity }} L</p>
139            <p><strong>Pressure:</strong> {{ oxygenSystem.mainTank.pressure }} bar</p>
140            <p><strong>Flow:</strong> {{ oxygenSystem.flow.rate }} L/min</p>
141            <p><strong>Consumers:</strong> {{ oxygenSystem.flow.consumers }}</p>
142          </div>
143          <div class="code-example">
144            <pre>const system = reactive({
145  mainTank: { level: 100 }
146})
147// Access: system.mainTank.level
148system.mainTank.level = 50</pre>
149          </div>
150        </div>
151      </div>
152    </div>
153
154    <div class="controls">
155      <button @click="startSimulation" class="btn btn-start">
156        ▶START SIMULATION
157      </button>
158      <button @click="stopSimulation" class="btn btn-stop">
159        ⏸STOP
160      </button>
161      <button @click="refill" class="btn btn-refill">
162        REFILL
163      </button>
164    </div>
165
166    <div class="info-box">
167      <h3>When to use ref() vs reactive()?</h3>
168      <div class="comparison-table">
169        <div class="comparison-col">
170          <h4>ref()</h4>
171          <ul>
172            <li>Primitive values (string, number, boolean)</li>
173            <li>When you want to replace the entire value</li>
174            <li>Easier to debug</li>
175            <li>Requires .value in script</li>
176          </ul>
177        </div>
178        <div class="comparison-col">
179          <h4>reactive()</h4>
180          <ul>
181            <li>Objects and arrays</li>
182            <li>Grouping related data</li>
183            <li>Does not require .value</li>
184            <li>Cannot replace the entire object</li>
185          </ul>
186        </div>
187      </div>
188    </div>
189  </div>
190</template>
191
192<style scoped>
193.oxygen-control {
194  min-height: 100vh;
195  background: #0a0e27;
196  color: #e0e0e0;
197  padding: 2rem;
198  font-family: 'Courier New', monospace;
199}
200
201.header {
202  text-align: center;
203  margin-bottom: 2rem;
204  border-bottom: 2px solid #00ff88;
205  padding-bottom: 1rem;
206}
207
208.header h1 {
209  color: #00ff88;
210  font-size: 2rem;
211  margin: 0;
212  text-shadow: 0 0 10px #00ff88;
213}
214
215.subtitle {
216  color: #00b4d8;
217  margin: 0.5rem 0 0 0;
218}
219
220.comparison-grid {
221  display: grid;
222  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
223  gap: 2rem;
224  margin-bottom: 2rem;
225}
226
227.section-title {
228  color: #00b4d8;
229  font-size: 1.3rem;
230  margin-bottom: 1rem;
231  text-align: center;
232}
233
234.card {
235  background: #1a1f3a;
236  padding: 1.5rem;
237  border-radius: 8px;
238  border: 1px solid #00b4d8;
239}
240
241.tank-visual {
242  width: 100%;
243  height: 200px;
244  background: #0a0e27;
245  border: 2px solid #00b4d8;
246  border-radius: 8px;
247  position: relative;
248  overflow: hidden;
249  margin-bottom: 1rem;
250}
251
252.tank-fill {
253  position: absolute;
254  bottom: 0;
255  width: 100%;
256  background: linear-gradient(180deg, #00ff88, #00b4d8);
257  transition: height 0.3s ease;
258  display: flex;
259  align-items: center;
260  justify-content: center;
261}
262
263.tank-fill.warning {
264  background: linear-gradient(180deg, #ffaa00, #ff8800);
265}
266
267.tank-fill.critical {
268  background: linear-gradient(180deg, #ff4444, #cc0000);
269}
270
271.tank-label {
272  color: #0a0e27;
273  font-weight: bold;
274  font-size: 1.5rem;
275  text-shadow: 0 0 5px rgba(255,255,255,0.5);
276}
277
278.tank-info {
279  margin-bottom: 1rem;
280}
281
282.tank-info p {
283  margin: 0.5rem 0;
284  color: #ccc;
285}
286
287.status-optimal {
288  color: #00ff88;
289}
290
291.status-warning {
292  color: #ffaa00;
293}
294
295.status-critical {
296  color: #ff4444;
297  animation: blink 1s infinite;
298}
299
300@keyframes blink {
301  0%, 50%, 100% { opacity: 1; }
302  25%, 75% { opacity: 0.5; }
303}
304
305.code-example {
306  background: #0a0e27;
307  padding: 1rem;
308  border-radius: 4px;
309  border-left: 3px solid #00ff88;
310}
311
312.code-example pre {
313  margin: 0;
314  color: #00ff88;
315  font-size: 0.85rem;
316  overflow-x: auto;
317}
318
319.controls {
320  display: flex;
321  gap: 1rem;
322  margin-bottom: 2rem;
323  justify-content: center;
324  flex-wrap: wrap;
325}
326
327.btn {
328  padding: 1rem 2rem;
329  font-size: 1rem;
330  border: none;
331  border-radius: 8px;
332  cursor: pointer;
333  font-weight: bold;
334  transition: all 0.3s ease;
335  font-family: 'Courier New', monospace;
336}
337
338.btn-start {
339  background: linear-gradient(135deg, #00ff88, #00b4d8);
340  color: #0a0e27;
341}
342
343.btn-stop {
344  background: linear-gradient(135deg, #ffaa00, #ff8800);
345  color: #0a0e27;
346}
347
348.btn-refill {
349  background: linear-gradient(135deg, #00b4d8, #0088cc);
350  color: white;
351}
352
353.btn:hover {
354  transform: scale(1.05);
355  box-shadow: 0 0 20px currentColor;
356}
357
358.info-box {
359  background: #1a1f3a;
360  padding: 1.5rem;
361  border-radius: 8px;
362  border-left: 4px solid #00ff88;
363}
364
365.info-box h3 {
366  color: #00ff88;
367  margin-top: 0;
368}
369
370.comparison-table {
371  display: grid;
372  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
373  gap: 1.5rem;
374  margin-top: 1rem;
375}
376
377.comparison-col h4 {
378  color: #00b4d8;
379  margin-top: 0;
380}
381
382.comparison-col ul {
383  padding-left: 1.5rem;
384  color: #ccc;
385}
386
387.comparison-col li {
388  margin: 0.5rem 0;
389}
390</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 reactive() do in Vue 3?

  2. 2. What is an advantage of reactive() over ref() for objects?

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 with station data (name, level, active) and display it in the template.

  • Horizontal ordering

    Arrange the elements of reactive() declaration in the correct order.

  • Code editor

    Create a reactive object and add functions to modify its properties (increase level, toggle status).

  • Click in order

    Arrange the steps for creating and mutating a reactive object in the correct order.

  • Vertical ordering

    Arrange the steps for working with reactive() in the correct order.

Useful articles