Vue.js course ยท Module 3: Reactivity
Reactivity in Vue - The Living Plasma Reactor
In this lesson5
Welcome to the NOVA LAB Plasma Reactor! Dr. Nova says: here sensors come alive - just as data in Vue becomes reactive and automatically updates the control interface. Every power, temperature, or pressure reading is instantly visible on the panels.
In module 2 you wrote templates that refreshed themselves after data changes, and you took that on faith. In this module we look inside that mechanism, because without understanding it you will sooner or later run into a panel that stubbornly shows an old reading.
What you will learn
- explain what reactivity is and how Vue tracks data changes,
- create reactive values with
ref()and use.valuedeliberately, - build reactive objects with
reactive()and work around its limitations, - choose deep or shallow reactivity:
shallowRef,shallowReactive,triggerRef, - unpack objects safely with
toRefandtoRefs, - diagnose state with
isRef,isReactive,unref,toRawandmarkRaw, - protect state with
readonly()and arrange it into a clear architecture.
What is Reactivity?
Let us start with the problem. In plain JavaScript, changing a variable leaves no trace, so the screen has no way to find out about it:
1let powerLevel = 0
2document.querySelector('#power').textContent = powerLevel
3
4powerLevel = 75
5// the screen still shows 0 - nobody knows about the changeTo refresh the panel, you would have to find the element and write the new value by hand after every change. Reactivity is a system that automatically tracks data changes and updates the DOM. When you change a value - like the reactor's power level - Vue "knows" and refreshes the view on the control panel.
1<template>
2 <div class="reactor-panel" style="background: #0a0e27; color: #00ff88;">
3 <p>Status: {{ reactorStatus }}</p>
4 <button @click="changeStatus">Change status</button>
5 </div>
6</template>
7
8<script setup>
9import { ref } from 'vue'
10
11const reactorStatus = ref('STANDBY')
12
13function changeStatus() {
14 reactorStatus.value = 'ACTIVE'
15 // Vue will automatically update the control panel!
16}
17</script>While rendering, the template reads reactorStatus, and Vue notes: this component depends on that sensor. When changeStatus assigns a new value, Vue re-renders the dependent component. DOM updates are batched and applied on the next tick (nextTick), so ten changes in a row result in a single refresh.
ref() - Reactive Sensor
ref() creates a reactive "wrapper" for a value - like a reactor sensor monitoring parameters. It takes an initial value of any type and returns a container with a .value property:
1import { ref } from 'vue'
2
3// For primitives - single readings
4const powerLevel = ref(0)
5const statusMessage = ref('System initializing...')
6const isOnline = ref(true)
7
8// For objects (also works) - complex sensor data
9const reactor = ref({
10 name: 'Plasma Core Alpha',
11 temperature: 2500
12})
13
14// Accessing the value - use .value
15console.log(powerLevel.value) // 0
16powerLevel.value++
17console.log(powerLevel.value) // 1
18
19// In template - no .value needed!
20// {{ powerLevel }} - Vue does this automaticallyIn the script you always write .value. Refs declared at the top level of <script setup> are unwrapped by the template automatically, so .value is unnecessary there. The variable stays const: the content of the container changes, not the container itself. In module 2 such a container also held a DOM element - it is still the same function, and the ref attribute was what put the element into it.
Why .value?
JavaScript cannot intercept reading or writing a plain variable, but it can intercept access to an object's property. ref() wraps the value in an object with a .value property - like a sensor packing raw data for transmission:
1const powerLevel = ref(0)
2
3// Under the hood:
4// powerLevel = { value: 0 }
5
6// Changing the value
7powerLevel.value = 75 // the .value setter detects the change โ panel updateThe { value: 0 } notation is a simplification. In reality .value has a getter that records who reads the value, and a setter that notifies those readers about a change. Objects created with reactive() in the next lesson work differently: there Vue uses the Proxy mechanism.
Typing ref (TypeScript)
In a component with <script setup lang="ts">, TypeScript usually infers the ref type on its own. You import the Ref<T> type with import type when you want to write it explicitly, and the Sensor interface describes the shape of the sensor data:
1import { ref } from 'vue'
2import type { Ref } from 'vue'
3
4// Automatic type inference
5const temperature = ref(0) // Ref<number>
6const sensorName = ref('Temperature Alpha') // Ref<string>
7
8// Explicit typing
9const missionYear: Ref<number> = ref(2087)
10
11// For complex types
12interface Sensor {
13 name: string
14 unit: string
15 maxValue: number
16}
17
18const temperatureSensor = ref<Sensor>({
19 name: 'Plasma Core Temperature',
20 unit: 'Kelvin',
21 maxValue: 5000
22})The ref<Sensor>() form passes the type in angle brackets. If you specify the type but omit the initial value, the type will also include undefined.
The Vue docs recommend ref() as the primary tool for declaring state, and that is how I work too. In the next lesson you will see reactive(), which covers a whole panel at once but comes with its own limitations.
Remember: ref() is a sensor in a capsule - in the script you open the capsule with .value, and the template hologram does it for you.
Code for this lesson: App.vue
1<script setup>
2import { ref, reactive } from 'vue'
3
4// Plasma Reactor - Basic reactivity
5// ref() for primitive values (numbers, strings, booleans)
6const reactorPower = ref(0)
7const isOnline = ref(false)
8const reactorStatus = ref('OFFLINE')
9
10// reactive() for objects
11const plasmaCore = reactive({
12 temperature: 0,
13 pressure: 0,
14 stability: 100,
15 fuelLevel: 100
16})
17
18function startReactor() {
19 isOnline.value = true
20 reactorStatus.value = 'INITIALIZING'
21
22 // Reactor startup simulation
23 const interval = setInterval(() => {
24 if (reactorPower.value < 100) {
25 reactorPower.value += 10
26 plasmaCore.temperature += 150
27 plasmaCore.pressure += 5
28 plasmaCore.fuelLevel -= 2
29 } else {
30 reactorStatus.value = 'ONLINE'
31 clearInterval(interval)
32 }
33 }, 300)
34}
35
36function shutdownReactor() {
37 isOnline.value = false
38 reactorStatus.value = 'OFFLINE'
39 reactorPower.value = 0
40 plasmaCore.temperature = 0
41 plasmaCore.pressure = 0
42 plasmaCore.stability = 100
43 plasmaCore.fuelLevel = 100
44}
45</script>
46
47<template>
48 <div class="reactor-control">
49 <div class="header">
50 <h1>PLASMA REACTOR - NOVA LAB</h1>
51 <p class="subtitle">Vue.js Reactivity System - ref() and reactive()</p>
52 </div>
53
54 <div class="status-grid">
55 <div class="status-card">
56 <div class="label">REACTOR STATUS</div>
57 <div :class="['value', isOnline ? 'online' : 'offline']">
58 {{ reactorStatus }}
59 </div>
60 </div>
61
62 <div class="status-card">
63 <div class="label">OUTPUT POWER</div>
64 <div class="value accent">{{ reactorPower }}%</div>
65 <div class="progress-bar">
66 <div class="progress-fill" :style="{ width: reactorPower + '%' }"></div>
67 </div>
68 </div>
69 </div>
70
71 <div class="plasma-grid">
72 <div class="plasma-card">
73 <div class="icon">โ</div>
74 <div class="label">TEMPERATURE</div>
75 <div class="value">{{ plasmaCore.temperature.toFixed(0) }}ยฐC</div>
76 </div>
77
78 <div class="plasma-card">
79 <div class="icon">โ</div>
80 <div class="label">PRESSURE</div>
81 <div class="value">{{ plasmaCore.pressure }} bar</div>
82 </div>
83
84 <div class="plasma-card">
85 <div class="icon">โ</div>
86 <div class="label">STABILITY</div>
87 <div class="value">{{ plasmaCore.stability }}%</div>
88 </div>
89
90 <div class="plasma-card">
91 <div class="icon">โ</div>
92 <div class="label">FUEL</div>
93 <div class="value">{{ plasmaCore.fuelLevel }}%</div>
94 </div>
95 </div>
96
97 <div class="controls">
98 <button
99 @click="startReactor"
100 :disabled="isOnline"
101 class="btn btn-start"
102 >
103 START REACTOR
104 </button>
105 <button
106 @click="shutdownReactor"
107 :disabled="!isOnline"
108 class="btn btn-stop"
109 >
110 SHUT DOWN REACTOR
111 </button>
112 </div>
113
114 <div class="info-box">
115 <h3>Learning Reactivity</h3>
116 <p><strong>ref():</strong> Used for primitive values (reactorPower, isOnline)</p>
117 <p><strong>reactive():</strong> Used for objects (plasmaCore)</p>
118 <p><strong>Access:</strong> ref requires .value, reactive does not</p>
119 </div>
120 </div>
121</template>
122
123<style scoped>
124.reactor-control {
125 min-height: 100vh;
126 background: #0a0e27;
127 color: #e0e0e0;
128 padding: 2rem;
129 font-family: 'Courier New', monospace;
130}
131
132.header {
133 text-align: center;
134 margin-bottom: 2rem;
135 border-bottom: 2px solid #00ff88;
136 padding-bottom: 1rem;
137}
138
139.header h1 {
140 color: #00ff88;
141 font-size: 2rem;
142 margin: 0;
143 text-shadow: 0 0 10px #00ff88;
144}
145
146.subtitle {
147 color: #00b4d8;
148 margin: 0.5rem 0 0 0;
149}
150
151.status-grid {
152 display: grid;
153 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
154 gap: 1rem;
155 margin-bottom: 2rem;
156}
157
158.status-card {
159 background: #1a1f3a;
160 padding: 1.5rem;
161 border-radius: 8px;
162 border: 1px solid #00b4d8;
163}
164
165.label {
166 font-size: 0.9rem;
167 color: #888;
168 margin-bottom: 0.5rem;
169}
170
171.value {
172 font-size: 1.8rem;
173 font-weight: bold;
174 color: #fff;
175}
176
177.value.online {
178 color: #00ff88;
179 text-shadow: 0 0 10px #00ff88;
180}
181
182.value.offline {
183 color: #ff4444;
184}
185
186.value.accent {
187 color: #00b4d8;
188}
189
190.progress-bar {
191 height: 8px;
192 background: #0a0e27;
193 border-radius: 4px;
194 margin-top: 0.5rem;
195 overflow: hidden;
196}
197
198.progress-fill {
199 height: 100%;
200 background: linear-gradient(90deg, #00b4d8, #00ff88);
201 transition: width 0.3s ease;
202}
203
204.plasma-grid {
205 display: grid;
206 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
207 gap: 1rem;
208 margin-bottom: 2rem;
209}
210
211.plasma-card {
212 background: #1a1f3a;
213 padding: 1.5rem;
214 border-radius: 8px;
215 border: 1px solid #333;
216 text-align: center;
217}
218
219.plasma-card .icon {
220 font-size: 2rem;
221 margin-bottom: 0.5rem;
222}
223
224.plasma-card .value {
225 font-size: 1.5rem;
226 color: #00ff88;
227}
228
229.controls {
230 display: flex;
231 gap: 1rem;
232 margin-bottom: 2rem;
233 justify-content: center;
234}
235
236.btn {
237 padding: 1rem 2rem;
238 font-size: 1.1rem;
239 border: none;
240 border-radius: 8px;
241 cursor: pointer;
242 font-weight: bold;
243 transition: all 0.3s ease;
244 font-family: 'Courier New', monospace;
245}
246
247.btn-start {
248 background: linear-gradient(135deg, #00ff88, #00b4d8);
249 color: #0a0e27;
250}
251
252.btn-start:hover:not(:disabled) {
253 transform: scale(1.05);
254 box-shadow: 0 0 20px #00ff88;
255}
256
257.btn-stop {
258 background: linear-gradient(135deg, #ff4444, #ff6666);
259 color: white;
260}
261
262.btn-stop:hover:not(:disabled) {
263 transform: scale(1.05);
264 box-shadow: 0 0 20px #ff4444;
265}
266
267.btn:disabled {
268 opacity: 0.5;
269 cursor: not-allowed;
270}
271
272.info-box {
273 background: #1a1f3a;
274 padding: 1.5rem;
275 border-radius: 8px;
276 border-left: 4px solid #00ff88;
277}
278
279.info-box h3 {
280 color: #00ff88;
281 margin-top: 0;
282}
283
284.info-box p {
285 margin: 0.5rem 0;
286 color: #ccc;
287}
288
289.info-box strong {
290 color: #00b4d8;
291}
292</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 is reactivity in Vue.js?
2. What is ref() used for in Vue 3?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create reactive variables using ref() for the station name and temperature, and display them in the template.
- Click in order
Arrange the steps for creating and using ref() in the correct order.
- Code editor
Create a counter with increment and decrement buttons using ref().
- Vertical ordering
Arrange the steps for creating reactive state with ref() in the correct order.