Vue.js course Β· Module 4: Computed & Watch
Watch - Monitoring System
In this lesson4
A computed answers the question "how much is it", but a good part of the station's work is not about values, it is about reactions. When an operator types a module name, the NOVA LAB database has to be queried. When the reactor temperature exceeds the norm, an alarm has to go off and the event has to be written to the log. Side effects like these must never go into a computed, because its getter should only calculate and return a result right away. Reacting to changes is the job of a watcher, the alarm sensor of Mission Control.
Basic watch()
The watch function takes two arguments and an optional third one. The first is the source, the thing we observe: a ref, an object from reactive, a getter, or an array of such sources. The second is the callback, which Vue calls after every change of the source, passing it the new and the previous value. A watcher is lazy by default: when created it does nothing and waits for the first change. Below is a module search in which the callback is an async function, and the database response delay is faked with await on a promise resolved by setTimeout.
1<template>
2 <div class="module-search" style="background: #0a0e27; color: #00ff88;">
3 <input v-model="searchQuery" placeholder="Search module..." style="background: #1a1e37; color: #00ff88;" />
4 <p v-if="isSearching" style="color: #00b4d8;">Searching...</p>
5 <ul>
6 <li v-for="result in searchResults" :key="result.id" style="border-bottom: 1px solid #00b4d8;">
7 {{ result.name }}
8 </li>
9 </ul>
10 </div>
11</template>
12
13<script setup>
14import { ref, watch } from 'vue'
15
16const searchQuery = ref('')
17const searchResults = ref([])
18const isSearching = ref(false)
19
20// Watch on a single value
21watch(searchQuery, async (newQuery, oldQuery) => {
22 console.log(`Search changed: "${oldQuery}" β "${newQuery}"`)
23
24 if (newQuery.length < 2) {
25 searchResults.value = []
26 return
27 }
28
29 isSearching.value = true
30
31 // Simulating API call to the NOVA LAB database
32 await new Promise(resolve => setTimeout(resolve, 500))
33
34 searchResults.value = [
35 { id: 1, name: `Module: ${newQuery}` }
36 ]
37
38 isSearching.value = false
39})
40</script>Type "ox", and a line with the old and new query appears in the console, followed half a second later by the search result. Queries shorter than two characters clear the list and end the callback with a return statement before anything is sent. What did not change: searchQuery is still an ordinary ref bound with v-model, and the watcher writes nothing into it, it only listens. Also notice the moment of the reaction. Vue does not call the callback on the same line where you change the value: it picks up the change and handles it in the next tick, before the DOM is refreshed. Two changes within one tick therefore produce a single call, already carrying the latest value.
Watch Options
The third argument is an options object. immediate: true runs the callback right away when the watcher is created, with the old value equal to undefined, which comes in handy for loading data when a panel starts. deep: true turns on deep observation of nested objects. flush picks the moment of the call: 'pre' (the default, before the DOM update), 'post' (after the update, when you want to read the refreshed DOM) or 'sync' (immediately on every change). once: true, available since Vue 3.4, stops the watcher after its first call.
1import { ref, watch } from 'vue'
2
3const count = ref(0)
4
5watch(count, (newValue) => {
6 console.log('Count:', newValue)
7}, {
8 immediate: true, // Call immediately on initialization
9 deep: true, // Deep observation of objects
10 flush: 'post', // When to execute: 'pre', 'post', 'sync'
11 once: true // Only once (Vue 3.4+)
12})This block is a cheat sheet, not a pattern to copy. count is a number, so deep changes nothing here, and combining immediate with once means the callback runs once, right at creation, and the watcher switches itself off immediately. In practice the configuration always goes in the same order: import watch from vue, the source, the callback, and at the end only the options you really need.
Watch on Multiple Sources
Life support systems are assessed together: oxygen and temperature. Instead of two watchers you pass an array of sources. The callback then receives an array of new values and an array of old ones, in the same order as the sources, and the destructuring [newO2, newTemp] names them on the spot.
1import { ref, watch } from 'vue'
2
3const oxygenLevel = ref(95)
4const temperature = ref(22)
5
6// Array of sources - monitoring multiple parameters
7watch([oxygenLevel, temperature], ([newO2, newTemp], [oldO2, oldTemp]) => {
8 console.log(`Life support changed:
9 O2: ${oldO2}% β ${newO2}%
10 Temp: ${oldTemp}Β°C β ${newTemp}Β°C`)
11})The callback runs when any of the sources changes. If only the oxygen changed, newTemp and oldTemp are equal. The watcher does not tell you which source moved, so compare the pairs yourself when you need to know.
Watch on Getter
An object from reactive calls for care. Writing watch(reactor.temperature, callback) will not work, because that way you pass the number 2500 rather than a reactive source, and Vue warns about an invalid watch source. You observe an object property through a getter, the function () => reactor.temperature, which Vue calls to read the current value.
1import { reactive, watch } from 'vue'
2
3const reactor = reactive({
4 name: 'Plasma Core Alpha',
5 temperature: 2500
6})
7
8// Watch on getter (function)
9watch(
10 () => reactor.temperature,
11 (newTemp) => {
12 console.log('New reactor temperature:', newTemp)
13 if (newTemp > 5000) {
14 console.warn('CRITICAL: Temperature exceeds safe limits!')
15 }
16 }
17)
18
19// Watch on deep object
20watch(
21 () => reactor,
22 (newReactor) => {
23 console.log('Reactor config changed:', newReactor)
24 },
25 { deep: true }
26)The first watcher reacts to the temperature only - renaming the reactor will not wake it up. The second observes a getter that returns the whole object, which is why it needs { deep: true }. Without that option it would not react to a change in any field, because a getter without deep wakes the watcher only when it returns a different value than before, and here it keeps returning the same object. A reactive object passed directly, without a getter, creates a deep watcher automatically. With a ref holding an object it is the other way round: a change to a nested field such as missionData.value.systems.oxygen goes unnoticed without deep: true. After such an in-place change, the new and the old value point to the same object, so comparing them tells you nothing.
Keep the cost in mind: on every run deep walks through all nested fields, which can get expensive on large structures. Since Vue 3.5 you can limit the depth with a number, for example deep: 2. Still, I recommend that you first reach for a getter that points at the specific field - a sensor wired to a single cable is cheaper than a scanner for the whole module. In the next lesson you will meet watchEffect, which finds its sources by itself.
Remember: watch is an alarm sensor - you point it at a source, and on every change it reports both the new and the previous reading.
Code for this lesson: App.vue
1<script setup>
2import { ref, watch } from 'vue'
3
4// NOVA LAB - System Monitoring with watch()
5const oxygenLevel = ref(95)
6const powerLevel = ref(2800)
7const waterLevel = ref(85)
8const logs = ref([])
9
10// watch() - reacts to specific source changes
11watch(oxygenLevel, (newValue, oldValue) => {
12 const timestamp = new Date().toLocaleTimeString()
13 logs.value.unshift({
14 time: timestamp,
15 type: 'oxygen',
16 message: `Oxygen: ${oldValue}% β ${newValue}%`,
17 color: newValue < 80 ? '#ff0044' : '#00ff88'
18 })
19
20 if (newValue < 80) {
21 logs.value.unshift({
22 time: timestamp,
23 type: 'warning',
24 message: 'WARNING: Low oxygen level detected!',
25 color: '#ffaa00'
26 })
27 }
28})
29
30watch(powerLevel, (newValue, oldValue) => {
31 const timestamp = new Date().toLocaleTimeString()
32 logs.value.unshift({
33 time: timestamp,
34 type: 'power',
35 message: `Power: ${oldValue}W β ${newValue}W`,
36 color: newValue < 2000 ? '#ff0044' : '#00b4d8'
37 })
38
39 if (newValue < 2000) {
40 logs.value.unshift({
41 time: timestamp,
42 type: 'critical',
43 message: 'CRITICAL: Power below minimum threshold!',
44 color: '#ff0044'
45 })
46 }
47})
48
49// Watch multiple sources
50watch([oxygenLevel, powerLevel, waterLevel], ([o2, power, water]) => {
51 const allOptimal = o2 > 90 && power > 2500 && water > 80
52 if (allOptimal) {
53 const timestamp = new Date().toLocaleTimeString()
54 logs.value.unshift({
55 time: timestamp,
56 type: 'success',
57 message: 'All systems optimal!',
58 color: '#00ff88'
59 })
60 }
61})
62
63function adjustOxygen(delta) {
64 oxygenLevel.value = Math.max(0, Math.min(100, oxygenLevel.value + delta))
65}
66
67function adjustPower(delta) {
68 powerLevel.value = Math.max(0, powerLevel.value + delta)
69}
70
71function adjustWater(delta) {
72 waterLevel.value = Math.max(0, Math.min(100, waterLevel.value + delta))
73}
74
75function clearLogs() {
76 logs.value = []
77}
78
79function simulateEmergency() {
80 oxygenLevel.value = 65
81 powerLevel.value = 1500
82 waterLevel.value = 45
83}
84
85function restoreNominal() {
86 oxygenLevel.value = 95
87 powerLevel.value = 2800
88 waterLevel.value = 85
89}
90</script>
91
92<template>
93 <div class="monitor-system">
94 <div class="header">
95 <h1>NOVA LAB System Monitor</h1>
96 <p class="subtitle">watch() API - Reactive Monitoring</p>
97 </div>
98
99 <div class="controls-panel">
100 <div class="system-control">
101 <h3>Oxygen System</h3>
102 <div class="level-bar">
103 <div
104 class="level-fill oxygen"
105 :style="{ width: oxygenLevel + '%' }"
106 ></div>
107 <span class="level-text">{{ oxygenLevel }}%</span>
108 </div>
109 <div class="buttons">
110 <button @click="adjustOxygen(-10)">-10%</button>
111 <button @click="adjustOxygen(-5)">-5%</button>
112 <button @click="adjustOxygen(5)">+5%</button>
113 <button @click="adjustOxygen(10)">+10%</button>
114 </div>
115 </div>
116
117 <div class="system-control">
118 <h3>Power System</h3>
119 <div class="level-bar">
120 <div
121 class="level-fill power"
122 :style="{ width: (powerLevel / 30) + '%' }"
123 ></div>
124 <span class="level-text">{{ powerLevel }}W</span>
125 </div>
126 <div class="buttons">
127 <button @click="adjustPower(-200)">-200W</button>
128 <button @click="adjustPower(-100)">-100W</button>
129 <button @click="adjustPower(100)">+100W</button>
130 <button @click="adjustPower(200)">+200W</button>
131 </div>
132 </div>
133
134 <div class="system-control">
135 <h3>Water Reserves</h3>
136 <div class="level-bar">
137 <div
138 class="level-fill water"
139 :style="{ width: waterLevel + '%' }"
140 ></div>
141 <span class="level-text">{{ waterLevel }}%</span>
142 </div>
143 <div class="buttons">
144 <button @click="adjustWater(-10)">-10%</button>
145 <button @click="adjustWater(-5)">-5%</button>
146 <button @click="adjustWater(5)">+5%</button>
147 <button @click="adjustWater(10)">+10%</button>
148 </div>
149 </div>
150 </div>
151
152 <div class="scenario-buttons">
153 <button @click="simulateEmergency" class="danger">Simulate Emergency</button>
154 <button @click="restoreNominal" class="success">Restore Nominal</button>
155 <button @click="clearLogs" class="neutral">Clear Logs</button>
156 </div>
157
158 <div class="log-panel">
159 <h3>System Event Log</h3>
160 <div class="log-entries">
161 <div
162 v-for="(log, index) in logs.slice(0, 15)"
163 :key="index"
164 class="log-entry"
165 :style="{ borderLeftColor: log.color, color: log.color }"
166 >
167 <span class="log-time">[{{ log.time }}]</span>
168 <span class="log-message">{{ log.message }}</span>
169 </div>
170 <div v-if="logs.length === 0" class="no-logs">
171 No events logged yet. Adjust system parameters to see watch() in action!
172 </div>
173 </div>
174 </div>
175
176 <div class="info-box">
177 <strong>watch() API:</strong> Monitors specific reactive sources and executes callbacks when they change.
178 Perfect for logging, side effects, and conditional responses!
179 </div>
180 </div>
181</template>
182
183<style>
184* {
185 margin: 0;
186 padding: 0;
187 box-sizing: border-box;
188}
189
190body {
191 font-family: 'Courier New', monospace;
192 background: #0a0e27;
193 color: #00ff88;
194 padding: 20px;
195}
196
197.monitor-system {
198 max-width: 1100px;
199 margin: 0 auto;
200}
201
202.header {
203 text-align: center;
204 margin-bottom: 30px;
205 padding: 20px;
206 background: linear-gradient(135deg, #1a1f4c 0%, #0a0e27 100%);
207 border: 2px solid #00ff88;
208 border-radius: 10px;
209}
210
211.header h1 {
212 font-size: 2rem;
213 margin-bottom: 10px;
214 text-shadow: 0 0 20px #00ff88;
215}
216
217.subtitle {
218 color: #00b4d8;
219 font-size: 1.1rem;
220}
221
222.controls-panel {
223 display: grid;
224 gap: 20px;
225 margin-bottom: 20px;
226}
227
228.system-control {
229 background: rgba(0, 180, 216, 0.05);
230 border: 2px solid #00b4d8;
231 border-radius: 10px;
232 padding: 20px;
233}
234
235.system-control h3 {
236 color: #00b4d8;
237 margin-bottom: 15px;
238}
239
240.level-bar {
241 position: relative;
242 height: 40px;
243 background: rgba(0, 0, 0, 0.5);
244 border: 1px solid #00ff88;
245 border-radius: 5px;
246 margin-bottom: 15px;
247 overflow: hidden;
248}
249
250.level-fill {
251 height: 100%;
252 transition: width 0.3s ease;
253}
254
255.level-fill.oxygen {
256 background: linear-gradient(90deg, #00ff88, #00cc66);
257}
258
259.level-fill.power {
260 background: linear-gradient(90deg, #00b4d8, #0088ff);
261}
262
263.level-fill.water {
264 background: linear-gradient(90deg, #0088ff, #0066cc);
265}
266
267.level-text {
268 position: absolute;
269 top: 50%;
270 left: 50%;
271 transform: translate(-50%, -50%);
272 font-weight: bold;
273 font-size: 1.2rem;
274 color: white;
275 text-shadow: 0 0 5px black;
276}
277
278.buttons {
279 display: flex;
280 gap: 10px;
281 justify-content: center;
282}
283
284button {
285 background: linear-gradient(135deg, #00b4d8 0%, #0077aa 100%);
286 color: white;
287 border: none;
288 padding: 10px 20px;
289 border-radius: 5px;
290 cursor: pointer;
291 font-family: 'Courier New', monospace;
292 font-weight: bold;
293 transition: all 0.3s ease;
294}
295
296button:hover {
297 transform: translateY(-2px);
298 box-shadow: 0 5px 15px rgba(0, 180, 216, 0.3);
299}
300
301.scenario-buttons {
302 display: flex;
303 gap: 15px;
304 justify-content: center;
305 margin-bottom: 20px;
306}
307
308.scenario-buttons button {
309 padding: 15px 30px;
310 font-size: 1rem;
311}
312
313.danger {
314 background: linear-gradient(135deg, #ff0044 0%, #cc0033 100%) !important;
315}
316
317.success {
318 background: linear-gradient(135deg, #00ff88 0%, #00cc66 100%) !important;
319}
320
321.neutral {
322 background: linear-gradient(135deg, #666 0%, #444 100%) !important;
323}
324
325.log-panel {
326 background: rgba(0, 0, 0, 0.5);
327 border: 2px solid #00ff88;
328 border-radius: 10px;
329 padding: 20px;
330 margin-bottom: 20px;
331}
332
333.log-panel h3 {
334 color: #00ff88;
335 margin-bottom: 15px;
336}
337
338.log-entries {
339 max-height: 300px;
340 overflow-y: auto;
341}
342
343.log-entry {
344 padding: 10px;
345 margin-bottom: 8px;
346 background: rgba(0, 255, 136, 0.05);
347 border-left: 4px solid;
348 border-radius: 4px;
349 font-size: 0.9rem;
350 animation: slideIn 0.3s ease;
351}
352
353@keyframes slideIn {
354 from {
355 opacity: 0;
356 transform: translateX(-20px);
357 }
358 to {
359 opacity: 1;
360 transform: translateX(0);
361 }
362}
363
364.log-time {
365 opacity: 0.7;
366 margin-right: 10px;
367}
368
369.no-logs {
370 text-align: center;
371 padding: 30px;
372 color: #666;
373 font-style: italic;
374}
375
376.info-box {
377 background: rgba(0, 255, 136, 0.1);
378 border: 2px solid #00ff88;
379 border-radius: 10px;
380 padding: 15px;
381 text-align: center;
382 color: #00ff88;
383 line-height: 1.6;
384}
385</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 the purpose of watch() in Vue.js?
2. What parameters does the callback in watch() receive?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Click in order
Arrange the correct watch syntax:
- Code editor
Complete the watch that observes searchQuery and logs changes with the new and old values.
- Vertical ordering
Arrange the watch configuration steps in the correct order:
- Code editor
Complete the watch with the immediate: true option to fetch data immediately after the watcher is created.
- Vertical ordering
Arrange the watch operation stages from source change to callback:
- Horizontal ordering
Arrange watch with the deep option in the correct order:
- Code editor
Complete the watch with { deep: true } to observe changes in a nested reactive object.