Vue.js course ยท Module 3: Reactivity

Reactivity in Vue - The Living Plasma Reactor

5 min read
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 .value deliberately,
  • build reactive objects with reactive() and work around its limitations,
  • choose deep or shallow reactivity: shallowRef, shallowReactive, triggerRef,
  • unpack objects safely with toRef and toRefs,
  • diagnose state with isRef, isReactive, unref, toRaw and markRaw,
  • 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 change

To 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 automatically

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

The { 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. 1. What is reactivity in Vue.js?

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

Useful articles