Vue.js course Β· Module 4: Computed & Watch
watchEffect - Automatic Tracking
In this lesson4
The engineer on duty added a third value, the battery level, to a watcher's message but forgot to add it to the array of sources. For a whole week the log showed outdated battery states, because the watcher woke up only when the first two parameters changed. With watch, the list of sources and the body of the callback live separately, so they easily drift out of sync. watchEffect removes that problem: you write only the effect, and Vue works out by itself which data it uses.
Basics of watchEffect
watchEffect takes a single function, called the effect. Vue runs it right away, at the moment the watcher is created, and during that run it records every reactive value the effect read. Those values become dependencies, and a change to any of them runs the effect again.
1<script setup>
2import { ref, watchEffect } from 'vue'
3
4const count = ref(0)
5const message = ref('Hello')
6
7// Automatically tracks count and message
8watchEffect(() => {
9 console.log(`Count: ${count.value}, Message: ${message.value}`)
10})
11
12// Will be called:
13// 1. Immediately on initialization
14// 2. When count changes
15// 3. When message changes
16</script>The console shows the first entry before you change anything, and further ones after every change of count or message. The dependencies, however, are only the values read during the latest run: if the effect reads something only inside an if branch that did not execute, a change to that value will not wake it up. Be careful with async effects too, because only the reads made before the first await are tracked. A watcher created in <script setup> stops on its own when the component unmounts, and you will learn the details of stopping in the next lesson.
watch vs watchEffect
Both tools can do the same job; they differ in how they work. Compare them on a single counter, observed in two ways at the same time.
1import { ref, watch, watchEffect } from 'vue'
2
3const count = ref(0)
4
5// watch - explicit source, access to oldValue
6watch(count, (newVal, oldVal) => {
7 console.log(`${oldVal} β ${newVal}`)
8})
9
10// watchEffect - automatic tracking, no oldValue
11watchEffect(() => {
12 console.log(`Count: ${count.value}`)
13})watch receives its source explicitly and provides the old value, so after the counter changes it prints 0 β 1. watchEffect prints just Count: 0 right at startup and then Count: 1, because it does not know the old value - it sees only the current state. The differences in a table:
| Feature | watch | watchEffect |
|---|---|---|
| Source | Explicit | Automatic |
| oldValue | Yes | No |
| immediate | Optional | Always |
| Lazy | Yes | No |
The "Lazy" row means that watch waits for the first change by default, while watchEffect starts immediately. Which one should you pick? I recommend watch in code that other people will read: the source is visible in the first line, like in a sensor specification. Reach for watchEffect when the effect uses several values, each of them should trigger it, and the old value is not needed.
Cleanup in watchEffect
Effects often start work that takes time: a server request, an interval, a connection. When id changes before the previous request returns, the late response can overwrite the new one. That is why the effect receives the onCleanup function as its first argument. You register a cleanup in it, and Vue calls it before the next run of the effect and when the watcher stops. An AbortController cancels the request: you pass its signal field to fetch, and its abort() method interrupts the request in flight.
1<script setup>
2import { ref, watchEffect } from 'vue'
3
4const id = ref(1)
5
6watchEffect((onCleanup) => {
7 const controller = new AbortController()
8
9 fetch(`/api/artwork/${id.value}`, {
10 signal: controller.signal
11 })
12 .then(res => res.json())
13 .then(data => console.log(data))
14
15 // Cleanup - called before each re-run
16 onCleanup(() => {
17 controller.abort()
18 })
19})
20</script>When id changes from 1 to 2, Vue first calls the cleanup and aborts the request for number 1, and only then runs the effect for number 2. Cleanup skips no run - the effect still reacts to every change. An aborted fetch rejects its promise with an AbortError, so in a real panel add handling for that one error. You will see the complete pattern in the next lesson. Since Vue 3.5 there is also an importable onWatcherCleanup() function, but it has to be called synchronously, before the first await.
watchPostEffect and watchSyncEffect
By default watchEffect works in flush: 'pre' mode, which means it runs the effect before the DOM update. When you need to read the already refreshed DOM, for example to measure the height of a panel after a row was added, use watchPostEffect, which is watchEffect with flush: 'post'. watchSyncEffect is the variant with flush: 'sync', which runs synchronously on every single change.
1import { watchEffect, watchPostEffect, watchSyncEffect } from 'vue'
2
3// Default - before DOM update
4watchEffect(() => { /* ... */ })
5
6// After DOM update
7watchPostEffect(() => {
8 // DOM is already updated here
9})
10
11// Synchronously - immediately on change
12watchSyncEffect(() => {
13 // Warning: may affect performance
14})The order from earliest to latest is fixed: sync immediately on the change, pre before the DOM update, post after it. The synchronous variant does not batch changes, so three assignments in a row mean three runs of the effect. Treat it as an exception, not the default choice.
Remember: watchEffect is the telemetry autopilot - it detects by itself which sensors it uses and starts right away, without waiting for the first change.
Code for this lesson: App.vue
1<script setup>
2import { ref, watchEffect } from 'vue'
3
4// NOVA LAB - Auto-Tracking System with watchEffect()
5const solarPanelAngle = ref(45)
6const sunIntensity = ref(70)
7const batteryCharge = ref(85)
8const powerConsumption = ref(1200)
9
10const logs = ref([])
11
12// watchEffect - automatically tracks ALL reactive dependencies
13watchEffect(() => {
14 // This will re-run whenever ANY dependency changes
15 const effectivePower = Math.floor(
16 (solarPanelAngle.value / 90) * sunIntensity.value * 30
17 )
18
19 const netPower = effectivePower - (powerConsumption.value / 100)
20
21 const timestamp = new Date().toLocaleTimeString()
22 logs.value.unshift({
23 time: timestamp,
24 message: `Power Generation: ${effectivePower}W | Net: ${netPower > 0 ? '+' : ''}${netPower.toFixed(0)}W`,
25 color: netPower > 0 ? '#00ff88' : '#ff0044'
26 })
27
28 // Auto-adjust battery based on net power
29 if (netPower > 0 && batteryCharge.value < 100) {
30 batteryCharge.value = Math.min(100, batteryCharge.value + 0.1)
31 } else if (netPower < 0 && batteryCharge.value > 0) {
32 batteryCharge.value = Math.max(0, batteryCharge.value - 0.1)
33 }
34})
35
36// Track battery status separately
37watchEffect(() => {
38 if (batteryCharge.value < 20) {
39 const timestamp = new Date().toLocaleTimeString()
40 logs.value.unshift({
41 time: timestamp,
42 message: 'WARNING: Battery critically low!',
43 color: '#ff0044'
44 })
45 }
46})
47
48const effectivePower = ref(0)
49
50watchEffect(() => {
51 effectivePower.value = Math.floor(
52 (solarPanelAngle.value / 90) * sunIntensity.value * 30
53 )
54})
55
56function optimizePanels() {
57 solarPanelAngle.value = 90
58}
59
60function nightMode() {
61 sunIntensity.value = 5
62 solarPanelAngle.value = 0
63}
64
65function dayMode() {
66 sunIntensity.value = 85
67 solarPanelAngle.value = 65
68}
69</script>
70
71<template>
72 <div class="auto-track">
73 <div class="header">
74 <h1>NOVA LAB Solar Power System</h1>
75 <p class="subtitle">watchEffect() - Auto-Dependency Tracking</p>
76 </div>
77
78 <div class="visual-panel">
79 <div class="solar-panel" :style="{ transform: `rotate(${solarPanelAngle}deg)` }">
80 <div class="panel-surface">
81 <div class="panel-grid"></div>
82 <div class="angle-display">{{ solarPanelAngle }}Β°</div>
83 </div>
84 </div>
85 <div class="sun-indicator" :style="{ opacity: sunIntensity / 100 }">
86 β
87 <div class="sun-rays"></div>
88 </div>
89 </div>
90
91 <div class="controls-grid">
92 <div class="control-card">
93 <h3>Sun Intensity</h3>
94 <input
95 v-model.number="sunIntensity"
96 type="range"
97 min="0"
98 max="100"
99 class="slider"
100 />
101 <div class="value-display">{{ sunIntensity }}%</div>
102 </div>
103
104 <div class="control-card">
105 <h3>Panel Angle</h3>
106 <input
107 v-model.number="solarPanelAngle"
108 type="range"
109 min="0"
110 max="90"
111 class="slider"
112 />
113 <div class="value-display">{{ solarPanelAngle }}Β°</div>
114 </div>
115
116 <div class="control-card">
117 <h3>Power Consumption</h3>
118 <input
119 v-model.number="powerConsumption"
120 type="range"
121 min="500"
122 max="2500"
123 step="100"
124 class="slider"
125 />
126 <div class="value-display">{{ powerConsumption }}W</div>
127 </div>
128
129 <div class="control-card battery">
130 <h3>Battery Charge</h3>
131 <div class="battery-indicator">
132 <div
133 class="battery-fill"
134 :style="{
135 width: batteryCharge + '%',
136 background: batteryCharge > 50 ? '#00ff88' : batteryCharge > 20 ? '#ffaa00' : '#ff0044'
137 }"
138 ></div>
139 </div>
140 <div class="value-display">{{ batteryCharge.toFixed(1) }}%</div>
141 </div>
142 </div>
143
144 <div class="stats-panel">
145 <div class="stat">
146 <span class="stat-label">Effective Power:</span>
147 <span class="stat-value power">{{ effectivePower }}W</span>
148 </div>
149 <div class="stat">
150 <span class="stat-label">Net Power:</span>
151 <span
152 class="stat-value"
153 :style="{ color: effectivePower - powerConsumption / 100 > 0 ? '#00ff88' : '#ff0044' }"
154 >
155 {{ effectivePower - Math.floor(powerConsumption / 100) > 0 ? '+' : '' }}{{ effectivePower - Math.floor(powerConsumption / 100) }}W
156 </span>
157 </div>
158 </div>
159
160 <div class="action-buttons">
161 <button @click="optimizePanels" class="optimize">Optimize Panels</button>
162 <button @click="nightMode" class="night">Night Mode</button>
163 <button @click="dayMode" class="day">Day Mode</button>
164 </div>
165
166 <div class="log-panel">
167 <h3>Auto-Tracking Log</h3>
168 <div class="log-list">
169 <div
170 v-for="(log, index) in logs.slice(0, 10)"
171 :key="index"
172 class="log-item"
173 :style="{ color: log.color }"
174 >
175 <span class="log-time">[{{ log.time }}]</span> {{ log.message }}
176 </div>
177 </div>
178 </div>
179
180 <div class="info-box">
181 <strong>watchEffect():</strong> Automatically tracks all reactive dependencies!
182 No need to specify what to watch - it figures it out on its own.
183 </div>
184 </div>
185</template>
186
187<style>
188* {
189 margin: 0;
190 padding: 0;
191 box-sizing: border-box;
192}
193
194body {
195 font-family: 'Courier New', monospace;
196 background: #0a0e27;
197 color: #00ff88;
198 padding: 20px;
199}
200
201.auto-track {
202 max-width: 1000px;
203 margin: 0 auto;
204}
205
206.header {
207 text-align: center;
208 margin-bottom: 30px;
209 padding: 20px;
210 background: linear-gradient(135deg, #1a1f4c 0%, #0a0e27 100%);
211 border: 2px solid #00ff88;
212 border-radius: 10px;
213}
214
215.header h1 {
216 font-size: 2rem;
217 margin-bottom: 10px;
218 text-shadow: 0 0 20px #00ff88;
219}
220
221.subtitle {
222 color: #00b4d8;
223 font-size: 1.1rem;
224}
225
226.visual-panel {
227 display: flex;
228 justify-content: space-around;
229 align-items: center;
230 padding: 40px;
231 margin-bottom: 30px;
232 background: rgba(0, 180, 216, 0.05);
233 border: 2px solid #00b4d8;
234 border-radius: 10px;
235 position: relative;
236}
237
238.solar-panel {
239 width: 150px;
240 height: 200px;
241 transition: transform 0.5s ease;
242 transform-origin: center center;
243}
244
245.panel-surface {
246 width: 100%;
247 height: 100%;
248 background: linear-gradient(135deg, #001a33 0%, #003366 100%);
249 border: 3px solid #00b4d8;
250 border-radius: 10px;
251 position: relative;
252 display: flex;
253 align-items: center;
254 justify-content: center;
255}
256
257.panel-grid {
258 position: absolute;
259 width: 90%;
260 height: 90%;
261 background-image:
262 repeating-linear-gradient(0deg, transparent, transparent 30px, #00b4d8 30px, #00b4d8 31px),
263 repeating-linear-gradient(90deg, transparent, transparent 30px, #00b4d8 30px, #00b4d8 31px);
264}
265
266.angle-display {
267 position: relative;
268 z-index: 1;
269 font-size: 1.5rem;
270 font-weight: bold;
271 color: #00ff88;
272 text-shadow: 0 0 10px #00ff88;
273}
274
275.sun-indicator {
276 font-size: 5rem;
277 filter: drop-shadow(0 0 30px #ffaa00);
278 transition: opacity 0.3s ease;
279 position: relative;
280}
281
282.controls-grid {
283 display: grid;
284 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
285 gap: 20px;
286 margin-bottom: 20px;
287}
288
289.control-card {
290 background: rgba(0, 180, 216, 0.05);
291 border: 2px solid #00b4d8;
292 border-radius: 10px;
293 padding: 20px;
294 text-align: center;
295}
296
297.control-card h3 {
298 color: #00b4d8;
299 margin-bottom: 15px;
300 font-size: 1rem;
301}
302
303.slider {
304 width: 100%;
305 margin: 15px 0;
306}
307
308.value-display {
309 font-size: 1.5rem;
310 font-weight: bold;
311 color: #00ff88;
312}
313
314.battery-indicator {
315 width: 100%;
316 height: 30px;
317 background: rgba(0, 0, 0, 0.5);
318 border: 2px solid #00ff88;
319 border-radius: 5px;
320 overflow: hidden;
321 margin: 15px 0;
322}
323
324.battery-fill {
325 height: 100%;
326 transition: all 0.3s ease;
327}
328
329.stats-panel {
330 display: flex;
331 justify-content: space-around;
332 padding: 20px;
333 margin-bottom: 20px;
334 background: rgba(0, 255, 136, 0.05);
335 border: 2px solid #00ff88;
336 border-radius: 10px;
337}
338
339.stat {
340 text-align: center;
341}
342
343.stat-label {
344 display: block;
345 color: #00b4d8;
346 margin-bottom: 10px;
347}
348
349.stat-value {
350 font-size: 2rem;
351 font-weight: bold;
352 text-shadow: 0 0 10px currentColor;
353}
354
355.stat-value.power {
356 color: #00ff88;
357}
358
359.action-buttons {
360 display: flex;
361 gap: 15px;
362 justify-content: center;
363 margin-bottom: 20px;
364}
365
366.action-buttons button {
367 padding: 15px 30px;
368 font-size: 1rem;
369 background: linear-gradient(135deg, #00b4d8 0%, #0077aa 100%);
370 color: white;
371 border: none;
372 border-radius: 5px;
373 cursor: pointer;
374 font-family: 'Courier New', monospace;
375 font-weight: bold;
376 transition: all 0.3s ease;
377}
378
379.action-buttons button:hover {
380 transform: translateY(-2px);
381 box-shadow: 0 5px 15px rgba(0, 180, 216, 0.3);
382}
383
384.optimize {
385 background: linear-gradient(135deg, #00ff88 0%, #00cc66 100%) !important;
386}
387
388.night {
389 background: linear-gradient(135deg, #333366 0%, #1a1a33 100%) !important;
390}
391
392.day {
393 background: linear-gradient(135deg, #ffaa00 0%, #ff8800 100%) !important;
394}
395
396.log-panel {
397 background: rgba(0, 0, 0, 0.5);
398 border: 2px solid #00ff88;
399 border-radius: 10px;
400 padding: 20px;
401 margin-bottom: 20px;
402}
403
404.log-panel h3 {
405 color: #00ff88;
406 margin-bottom: 15px;
407}
408
409.log-list {
410 max-height: 200px;
411 overflow-y: auto;
412}
413
414.log-item {
415 padding: 8px;
416 margin-bottom: 5px;
417 background: rgba(0, 255, 136, 0.05);
418 border-radius: 4px;
419 font-size: 0.9rem;
420 animation: fadeIn 0.3s ease;
421}
422
423@keyframes fadeIn {
424 from { opacity: 0; }
425 to { opacity: 1; }
426}
427
428.log-time {
429 opacity: 0.7;
430}
431
432.info-box {
433 background: rgba(0, 255, 136, 0.1);
434 border: 2px solid #00ff88;
435 border-radius: 10px;
436 padding: 15px;
437 text-align: center;
438 color: #00ff88;
439 line-height: 1.6;
440}
441</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 key difference between watch and watchEffect?
2. When does watchEffect execute for the first time?
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 watchEffect syntax:
- Code editor
Complete the watchEffect that automatically tracks changes in multiple refs and logs them.
- Vertical ordering
Arrange the watchEffect lifecycle from creation to cleanup:
- Code editor
Complete the watch observing an array of multiple sources [sensorA, sensorB, sensorC].