Vue.js course · Module 3: Reactivity
reactive() - Reactive Systems
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 updateThe 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) // trueThe 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:
| Feature | ref() | reactive() |
|---|---|---|
| For primitives | Yes | No |
| For objects | Yes (.value) | Yes (directly) |
| Destructuring | Safe (you pass the whole container) | Loses reactivity |
| Reassign | Yes (.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 reactiveDestructuring 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. What does reactive() do in Vue 3?
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.