Vue.js course · Module 4: Computed & Watch

Computed and Watch in Practice - Mission Systems

8 min read
In this lesson4

Mission Control has ordered three panels: a module search that does not bombard the server with a request after every letter, a trajectory calculator with a history of results, and a registration form with validation. Each one combines computed with a watcher, and the whole art lies in not mixing up the roles. The rule is short: when you need a value, write a computed, and when you need an action - a request, a log entry, a timer - write a watch. The computed getter must stay pure: it only calculates and returns a result, it does not send requests, does not write to localStorage and does not change other data.

Module Search System with Debounce

An operator typing the word "reactor" would send seven requests, although only the last one matters. The debounce technique holds the action back until the data stops changing for a set time, here 300 milliseconds. Let us start with the template: a search field, a loading message and a list of results.

1<template>
2  <div class="module-search-system" style="background: #0a0e27; color: #00ff88;">
3    <input v-model="searchInput" placeholder="Search modules..." style="background: #1a1e37; color: #00ff88;" />
4    <p v-if="isLoading" style="color: #00b4d8;">Scanning NOVA LAB database...</p>
5    <ul>
6      <li v-for="item in results" :key="item.id" style="border-bottom: 1px solid #00b4d8;">
7        {{ item.name }}
8      </li>
9    </ul>
10  </div>
11</template>

The field is bound to searchInput, the raw text that changes after every keystroke. The list reads results, and the message shows up while isLoading is true. Now the script with the first watcher, which delays copying the text into searchQuery.

1<script setup>
2import { ref, watch } from 'vue'
3
4const searchInput = ref('')
5const searchQuery = ref('')
6const results = ref([])
7const isLoading = ref(false)
8
9// Debounce - delay the search
10let debounceTimeout
11watch(searchInput, (value) => {
12  clearTimeout(debounceTimeout)
13  debounceTimeout = setTimeout(() => {
14    searchQuery.value = value
15  }, 300)
16})

The order of steps is worth memorizing: the debounceTimeout variable outside the watcher, then watch(searchInput, ...), inside it clearTimeout cancelling the previous countdown and setTimeout starting a new one. Every keystroke resets the clock, so only text followed by a pause reaches searchQuery. The field on screen still shows the letters immediately - only the search waits. The second watcher performs the actual request.

1// Execute search
2watch(searchQuery, async (query) => {
3  if (!query) {
4    results.value = []
5    return
6  }
7
8  isLoading.value = true
9
10  try {
11    const response = await fetch(`/api/nova-lab/modules?q=${query}`)
12    results.value = await response.json()
13  } finally {
14    isLoading.value = false
15  }
16})
17</script>

The callback is async, and the try/finally block guarantees that isLoading returns to false even after a network error. Asynchrony has a price, though: if an older request comes back later than a newer one, it overwrites the fresh results. So in a production panel, add cleanup here with onCleanup and AbortController, just as in the lesson on stopping watchers.

Trajectory Calculator with History

The second panel computes an operation on velocity and time and keeps a history of results. The result is a value, so it goes into a computed, and a history entry is an action, so it goes into a watcher. The template contains two number fields, a list with the operation and the history.

1<template>
2  <div class="trajectory-calc" style="background: #0a0e27; color: #00ff88;">
3    <label>Velocity (km/s):</label>
4    <input v-model.number="velocity" type="number" style="background: #1a1e37; color: #00ff88;" />
5
6    <label>Operation:</label>
7    <select v-model="operation" style="background: #1a1e37; color: #00ff88;">
8      <option value="+">Add</option>
9      <option value="-">Subtract</option>
10      <option value="*">Multiply</option>
11      <option value="/">Divide</option>
12    </select>
13
14    <label>Time (s):</label>
15    <input v-model.number="time" type="number" style="background: #1a1e37; color: #00ff88;" />
16    <p style="color: #00b4d8;">= {{ result }}</p>
17
18    <h3>Calculation history:</h3>
19    <ul>
20      <li v-for="(entry, i) in history" :key="i" style="color: #00b4d8;">{{ entry }}</li>
21    </ul>
22  </div>
23</template>

The fields use v-model.number, so numbers rather than text reach the variables, and the history list renders its entries below the result. The first part of the script holds the panel state and the computed with the result.

1<script setup>
2import { ref, computed, watch } from 'vue'
3
4const velocity = ref(25)
5const time = ref(3600)
6const operation = ref('*')
7const history = ref([])
8
9const result = computed(() => {
10  switch (operation.value) {
11    case '+': return velocity.value + time.value
12    case '-': return velocity.value - time.value
13    case '*': return velocity.value * time.value
14    case '/': return time.value !== 0 ? velocity.value / time.value : 'Error'
15    default: return 0
16  }
17})

The switch statement picks the operation, and when dividing by zero the computed returns the text Error instead of infinity. The second part of the script adds results to the history.

1// Save to calculation history
2watch(result, (newResult) => {
3  const entry = `${velocity.value} ${operation.value} ${time.value} = ${newResult}`
4  history.value.unshift(entry)
5
6  // Limit to 10 entries
7  if (history.value.length > 10) {
8    history.value.pop()
9  }
10})
11</script>

The watcher observes the result computed, because a computed is a perfectly valid source for watch. The unshift method adds an entry at the beginning of the array, and pop removes the oldest one once there are more than 10 entries. Note something less than obvious: when the velocity is 0, changing the time while multiplying adds no entry, because the result is still 0 and the watcher reacts only to a change of value.

Form Validation

The third panel is a registration form with an email address and a password. Computed alone is enough here, because every message and every button state is a value derived from what the user typed. The template binds the fields, shows errors and blocks the button.

1<template>
2  <form @submit.prevent="submit">
3    <div>
4      <input v-model="email" placeholder="Email" />
5      <span v-if="emailError" class="error">{{ emailError }}</span>
6    </div>
7
8    <div>
9      <input v-model="password" type="password" placeholder="Password" />
10      <div class="strength" :class="passwordStrengthClass">
11        Password strength: {{ passwordStrength }}
12      </div>
13    </div>
14
15    <button :disabled="!isFormValid">Register</button>
16  </form>
17</template>

The button gets :disabled="!isFormValid", so it unlocks by itself once the form becomes valid, and :class switches the look of the password strength indicator. The first part of the script defines the fields and two computed messages.

1<script setup>
2import { ref, computed } from 'vue'
3
4const email = ref('')
5const password = ref('')
6
7const emailError = computed(() => {
8  if (!email.value) return ''
9  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
10  return regex.test(email.value) ? '' : 'Invalid email'
11})
12
13const passwordStrength = computed(() => {
14  const pwd = password.value
15  if (pwd.length < 6) return 'Weak'
16  if (pwd.length < 10) return 'Medium'
17  if (/[A-Z]/.test(pwd) && /[0-9]/.test(pwd)) return 'Strong'
18  return 'Medium'
19})

emailError first checks whether the field is empty, because an empty form should not shout an error before the user starts typing. Then a regular expression tests whether the address looks like "something@something.something" with no spaces. passwordStrength rates the password by its length and by the presence of an uppercase letter and a digit. The second part of the script builds on these results.

1const passwordStrengthClass = computed(() => ({
2  weak: passwordStrength.value === 'Weak',
3  medium: passwordStrength.value === 'Medium',
4  strong: passwordStrength.value === 'Strong'
5}))
6
7const isFormValid = computed(() => {
8  return email.value &&
9         !emailError.value &&
10         password.value.length >= 6
11})
12
13function submit() {
14  console.log('Form submitted')
15}
16</script>

passwordStrengthClass reads passwordStrength, and isFormValid reads emailError - that is a computed chain, in which each link recalculates only after the previous link changes. The same mechanism in its simplest form looks like this:

1const price = ref(100)
2const tax = computed(() => price.value * 0.23)
3const total = computed(() => price.value + tax.value)

Changing price invalidates both computed values, and total recalculates with the new tax on its next read. The chain needs no watcher and no manual synchronization.

Debugging computed and watchers

When a computed does not recalculate when you expect it to, start with Vue DevTools. It is a browser extension, also available as a Vite plugin and as a standalone app, that shows the component tree and the state of each component, including the current values of refs and computed properties. For precise tracking there are the onTrack and onTrigger options, passed as the second argument of computed or among the options of watch.

1const tax = computed(() => price.value * 0.23, {
2  onTrack(event) {
3    console.log('tracked', event.key)
4  },
5  onTrigger(event) {
6    console.log('triggered', event.key, event.newValue)
7  }
8})

onTrack fires when the getter registers a dependency, and onTrigger when a change of that dependency invalidates the result, provided the computed is actually in use, for example in a template. Both options work only in development mode, so in the production build they cost nothing. I recommend DevTools first, and onTrack with onTrigger only when the state inspector is not enough. In the module project you will combine all of these tools in a single mission dashboard.

Remember: a computed answers the question "how much is it", and a watch answers "what to do when it changes" - mix up those roles, and the mission panel either calculates too much or does not react at all.

Code for this lesson: App.vue
1<script setup>
2import { ref, watch, watchEffect, onUnmounted } from 'vue'
3
4// NOVA LAB - Advanced Watching: Multiple Sources & Cleanup
5const sensorA = ref(75)
6const sensorB = ref(82)
7const sensorC = ref(91)
8const isMonitoring = ref(false)
9const alerts = ref([])
10const sensorReadings = ref([])
11
12let monitoringInterval = null
13
14// Watch multiple sources
15watch([sensorA, sensorB, sensorC], ([a, b, c], [oldA, oldB, oldC]) => {
16  const timestamp = new Date().toLocaleTimeString()
17
18  if (a !== oldA) {
19    alerts.value.unshift({
20      time: timestamp,
21      sensor: 'A',
22      value: a,
23      previous: oldA,
24      color: '#00ff88'
25    })
26  }
27  if (b !== oldB) {
28    alerts.value.unshift({
29      time: timestamp,
30      sensor: 'B',
31      value: b,
32      previous: oldB,
33      color: '#00b4d8'
34    })
35  }
36  if (c !== oldC) {
37    alerts.value.unshift({
38      time: timestamp,
39      sensor: 'C',
40      value: c,
41      previous: oldC,
42      color: '#ffaa00'
43    })
44  }
45
46  // Calculate average
47  const avg = Math.round((a + b + c) / 3)
48  if (avg < 70) {
49    alerts.value.unshift({
50      time: timestamp,
51      sensor: 'SYSTEM',
52      value: avg,
53      message: 'Average below threshold!',
54      color: '#ff0044'
55    })
56  }
57})
58
59// Watch with cleanup function
60watch(isMonitoring, (newValue, oldValue, onCleanup) => {
61  const timestamp = new Date().toLocaleTimeString()
62
63  if (newValue) {
64    alerts.value.unshift({
65      time: timestamp,
66      message: '▶Continuous monitoring started',
67      color: '#00ff88'
68    })
69
70    // Start interval
71    monitoringInterval = setInterval(() => {
72      const reading = {
73        time: new Date().toLocaleTimeString(),
74        a: sensorA.value,
75        b: sensorB.value,
76        c: sensorC.value,
77        avg: Math.round((sensorA.value + sensorB.value + sensorC.value) / 3)
78      }
79      sensorReadings.value.unshift(reading)
80
81      // Simulate sensor fluctuation
82      sensorA.value = Math.max(0, Math.min(100, sensorA.value + (Math.random() - 0.5) * 2))
83      sensorB.value = Math.max(0, Math.min(100, sensorB.value + (Math.random() - 0.5) * 2))
84      sensorC.value = Math.max(0, Math.min(100, sensorC.value + (Math.random() - 0.5) * 2))
85    }, 2000)
86
87    // Cleanup function - called when watcher stops or before re-run
88    onCleanup(() => {
89      if (monitoringInterval) {
90        clearInterval(monitoringInterval)
91        const stopTime = new Date().toLocaleTimeString()
92        alerts.value.unshift({
93          time: stopTime,
94          message: '⏸Monitoring stopped (cleanup executed)',
95          color: '#ffaa00'
96        })
97      }
98    })
99  }
100})
101
102// watchEffect for computed average
103const avgReading = ref(0)
104watchEffect(() => {
105  avgReading.value = Math.round((sensorA.value + sensorB.value + sensorC.value) / 3)
106})
107
108function adjustSensor(sensor, delta) {
109  if (sensor === 'A') {
110    sensorA.value = Math.max(0, Math.min(100, sensorA.value + delta))
111  } else if (sensor === 'B') {
112    sensorB.value = Math.max(0, Math.min(100, sensorB.value + delta))
113  } else if (sensor === 'C') {
114    sensorC.value = Math.max(0, Math.min(100, sensorC.value + delta))
115  }
116}
117
118function toggleMonitoring() {
119  isMonitoring.value = !isMonitoring.value
120}
121
122function resetSensors() {
123  sensorA.value = 75
124  sensorB.value = 82
125  sensorC.value = 91
126}
127
128function clearAlerts() {
129  alerts.value = []
130  sensorReadings.value = []
131}
132
133// Cleanup on unmount
134onUnmounted(() => {
135  if (monitoringInterval) {
136    clearInterval(monitoringInterval)
137  }
138})
139</script>
140
141<template>
142  <div class="advanced-watch">
143    <div class="header">
144      <h1>NOVA LAB Sensor Array</h1>
145      <p class="subtitle">Multiple Sources & Watch Cleanup</p>
146    </div>
147
148    <div class="monitoring-control">
149      <button
150        @click="toggleMonitoring"
151        :class="{ active: isMonitoring }"
152        class="monitor-btn"
153      >
154        {{ isMonitoring ? '⏸Stop Monitoring' : '▶Start Monitoring' }}
155      </button>
156      <div class="status" :class="{ monitoring: isMonitoring }">
157        {{ isMonitoring ? 'ACTIVE' : 'INACTIVE' }}
158      </div>
159    </div>
160
161    <div class="sensors-grid">
162      <div class="sensor-card sensor-a">
163        <h3>Sensor A</h3>
164        <div class="sensor-visual">
165          <div
166            class="sensor-bar"
167            :style="{ height: sensorA + '%', background: '#00ff88' }"
168          ></div>
169        </div>
170        <div class="sensor-value">{{ sensorA.toFixed(1) }}%</div>
171        <div class="sensor-controls">
172          <button @click="adjustSensor('A', -5)">-5</button>
173          <button @click="adjustSensor('A', 5)">+5</button>
174        </div>
175      </div>
176
177      <div class="sensor-card sensor-b">
178        <h3>Sensor B</h3>
179        <div class="sensor-visual">
180          <div
181            class="sensor-bar"
182            :style="{ height: sensorB + '%', background: '#00b4d8' }"
183          ></div>
184        </div>
185        <div class="sensor-value">{{ sensorB.toFixed(1) }}%</div>
186        <div class="sensor-controls">
187          <button @click="adjustSensor('B', -5)">-5</button>
188          <button @click="adjustSensor('B', 5)">+5</button>
189        </div>
190      </div>
191
192      <div class="sensor-card sensor-c">
193        <h3>Sensor C</h3>
194        <div class="sensor-visual">
195          <div
196            class="sensor-bar"
197            :style="{ height: sensorC + '%', background: '#ffaa00' }"
198          ></div>
199        </div>
200        <div class="sensor-value">{{ sensorC.toFixed(1) }}%</div>
201        <div class="sensor-controls">
202          <button @click="adjustSensor('C', -5)">-5</button>
203          <button @click="adjustSensor('C', 5)">+5</button>
204        </div>
205      </div>
206
207      <div class="sensor-card average">
208        <h3>Average</h3>
209        <div class="avg-display">{{ avgReading }}%</div>
210        <div class="avg-status" :style="{
211          color: avgReading > 80 ? '#00ff88' : avgReading > 60 ? '#ffaa00' : '#ff0044'
212        }">
213          {{ avgReading > 80 ? 'OPTIMAL' : avgReading > 60 ? 'CAUTION' : 'CRITICAL' }}
214        </div>
215      </div>
216    </div>
217
218    <div class="quick-actions">
219      <button @click="resetSensors">Reset Sensors</button>
220      <button @click="clearAlerts">Clear Logs</button>
221    </div>
222
223    <div class="data-panels">
224      <div class="panel alerts-panel">
225        <h3>Alert Log</h3>
226        <div class="panel-content">
227          <div
228            v-for="(alert, index) in alerts.slice(0, 10)"
229            :key="index"
230            class="alert-item"
231            :style="{ borderLeftColor: alert.color }"
232          >
233            <span class="alert-time">[{{ alert.time }}]</span>
234            <span v-if="alert.sensor" class="alert-sensor">Sensor {{ alert.sensor }}:</span>
235            <span v-if="alert.value !== undefined" class="alert-value">
236              {{ alert.previous !== undefined ? `${alert.previous} → ` : '' }}{{ alert.value }}%
237            </span>
238            <span v-if="alert.message" class="alert-message">{{ alert.message }}</span>
239          </div>
240          <div v-if="alerts.length === 0" class="no-data">
241            No alerts yet...
242          </div>
243        </div>
244      </div>
245
246      <div class="panel readings-panel">
247        <h3>Sensor Readings (Auto-capture)</h3>
248        <div class="panel-content">
249          <div
250            v-for="(reading, index) in sensorReadings.slice(0, 8)"
251            :key="index"
252            class="reading-item"
253          >
254            <span class="reading-time">[{{ reading.time }}]</span>
255            <span class="reading-data">
256              A: {{ reading.a.toFixed(1) }}% |
257              B: {{ reading.b.toFixed(1) }}% |
258              C: {{ reading.c.toFixed(1) }}% |
259              Avg: {{ reading.avg }}%
260            </span>
261          </div>
262          <div v-if="sensorReadings.length === 0" class="no-data">
263            Start monitoring to capture readings...
264          </div>
265        </div>
266      </div>
267    </div>
268
269    <div class="info-box">
270      <strong>Advanced Watching:</strong> Watch multiple sources with <code>watch([a, b, c], ...)</code>.
271      Use <code>onCleanup()</code> to clean up side effects like intervals when watcher stops!
272    </div>
273  </div>
274</template>
275
276<style>
277* {
278  margin: 0;
279  padding: 0;
280  box-sizing: border-box;
281}
282
283body {
284  font-family: 'Courier New', monospace;
285  background: #0a0e27;
286  color: #00ff88;
287  padding: 20px;
288}
289
290.advanced-watch {
291  max-width: 1200px;
292  margin: 0 auto;
293}
294
295.header {
296  text-align: center;
297  margin-bottom: 30px;
298  padding: 20px;
299  background: linear-gradient(135deg, #1a1f4c 0%, #0a0e27 100%);
300  border: 2px solid #00ff88;
301  border-radius: 10px;
302}
303
304.header h1 {
305  font-size: 2rem;
306  margin-bottom: 10px;
307  text-shadow: 0 0 20px #00ff88;
308}
309
310.subtitle {
311  color: #00b4d8;
312  font-size: 1.1rem;
313}
314
315.monitoring-control {
316  display: flex;
317  justify-content: center;
318  align-items: center;
319  gap: 20px;
320  margin-bottom: 30px;
321}
322
323.monitor-btn {
324  padding: 15px 40px;
325  font-size: 1.2rem;
326  background: linear-gradient(135deg, #00b4d8 0%, #0077aa 100%);
327  color: white;
328  border: none;
329  border-radius: 8px;
330  cursor: pointer;
331  font-family: 'Courier New', monospace;
332  font-weight: bold;
333  transition: all 0.3s ease;
334}
335
336.monitor-btn.active {
337  background: linear-gradient(135deg, #00ff88 0%, #00cc66 100%);
338  animation: pulse 2s infinite;
339}
340
341@keyframes pulse {
342  0%, 100% { box-shadow: 0 0 20px rgba(0, 255, 136, 0.5); }
343  50% { box-shadow: 0 0 40px rgba(0, 255, 136, 0.8); }
344}
345
346.status {
347  font-size: 1.5rem;
348  font-weight: bold;
349  color: #666;
350}
351
352.status.monitoring {
353  color: #00ff88;
354  text-shadow: 0 0 10px #00ff88;
355}
356
357.sensors-grid {
358  display: grid;
359  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
360  gap: 20px;
361  margin-bottom: 20px;
362}
363
364.sensor-card {
365  background: rgba(0, 180, 216, 0.05);
366  border: 2px solid #00b4d8;
367  border-radius: 10px;
368  padding: 20px;
369  text-align: center;
370}
371
372.sensor-card h3 {
373  color: #00b4d8;
374  margin-bottom: 15px;
375}
376
377.sensor-visual {
378  height: 150px;
379  background: rgba(0, 0, 0, 0.5);
380  border: 2px solid #00ff88;
381  border-radius: 8px;
382  margin-bottom: 15px;
383  display: flex;
384  align-items: flex-end;
385  justify-content: center;
386  overflow: hidden;
387}
388
389.sensor-bar {
390  width: 60%;
391  transition: height 0.3s ease;
392  border-radius: 5px 5px 0 0;
393}
394
395.sensor-value {
396  font-size: 1.8rem;
397  font-weight: bold;
398  color: #00ff88;
399  margin-bottom: 10px;
400}
401
402.sensor-controls {
403  display: flex;
404  gap: 10px;
405  justify-content: center;
406}
407
408.sensor-controls button {
409  background: #00b4d8;
410  color: white;
411  border: none;
412  padding: 8px 15px;
413  border-radius: 5px;
414  cursor: pointer;
415  font-family: 'Courier New', monospace;
416  font-weight: bold;
417}
418
419.average {
420  grid-column: span 1;
421}
422
423.avg-display {
424  font-size: 3rem;
425  font-weight: bold;
426  color: #00ff88;
427  text-shadow: 0 0 20px #00ff88;
428  margin: 20px 0;
429}
430
431.avg-status {
432  font-size: 1.2rem;
433  font-weight: bold;
434}
435
436.quick-actions {
437  display: flex;
438  gap: 15px;
439  justify-content: center;
440  margin-bottom: 20px;
441}
442
443.quick-actions button {
444  padding: 12px 30px;
445  background: linear-gradient(135deg, #666 0%, #444 100%);
446  color: white;
447  border: none;
448  border-radius: 5px;
449  cursor: pointer;
450  font-family: 'Courier New', monospace;
451  font-weight: bold;
452}
453
454.data-panels {
455  display: grid;
456  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
457  gap: 20px;
458  margin-bottom: 20px;
459}
460
461.panel {
462  background: rgba(0, 0, 0, 0.5);
463  border: 2px solid #00ff88;
464  border-radius: 10px;
465  padding: 20px;
466}
467
468.panel h3 {
469  color: #00ff88;
470  margin-bottom: 15px;
471}
472
473.panel-content {
474  max-height: 300px;
475  overflow-y: auto;
476}
477
478.alert-item,
479.reading-item {
480  padding: 10px;
481  margin-bottom: 8px;
482  background: rgba(0, 255, 136, 0.05);
483  border-radius: 4px;
484  font-size: 0.9rem;
485  animation: slideIn 0.3s ease;
486}
487
488.alert-item {
489  border-left: 4px solid;
490}
491
492@keyframes slideIn {
493  from {
494    opacity: 0;
495    transform: translateX(-10px);
496  }
497  to {
498    opacity: 1;
499    transform: translateX(0);
500  }
501}
502
503.alert-time,
504.reading-time {
505  opacity: 0.7;
506  margin-right: 8px;
507}
508
509.alert-sensor {
510  color: #00b4d8;
511  font-weight: bold;
512  margin-right: 5px;
513}
514
515.alert-value {
516  color: #00ff88;
517}
518
519.reading-data {
520  color: #00b4d8;
521}
522
523.no-data {
524  text-align: center;
525  padding: 30px;
526  color: #666;
527  font-style: italic;
528}
529
530.info-box {
531  background: rgba(0, 255, 136, 0.1);
532  border: 2px solid #00ff88;
533  border-radius: 10px;
534  padding: 15px;
535  text-align: center;
536  color: #00ff88;
537  line-height: 1.6;
538}
539
540code {
541  background: rgba(0, 0, 0, 0.3);
542  padding: 2px 8px;
543  border-radius: 3px;
544  color: #00b4d8;
545}
546</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. When is it better to use computed instead of watch?

  2. 2. When is it better to use watch instead of computed?

These are 2 of 10 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Complete the debounced search using watch to delay sending the query.

  • Click in order

    Arrange the correct import of computed and watch from Vue:

  • Vertical ordering

    Arrange the watch flush modes from earliest to latest execution:

  • Horizontal ordering

    Arrange the watch observing a reactive object property:

  • Click in order

    Arrange the watch observing multiple refs simultaneously:

  • Vertical ordering

    Arrange the steps for creating a computed property from start to finish:

  • Horizontal ordering

    Arrange the correct destructuring in watch on multiple sources:

  • Click in order

    Arrange watchEffect with onCleanup in the correct order:

  • Horizontal ordering

    Arrange the computed for email validation in the correct order:

  • Vertical ordering

    Arrange the steps for implementing debounce with watch from start to finish:

  • Click in order

    Arrange the computed dependency chain from source to final value:

  • Horizontal ordering

    Arrange the correct watchPostEffect call:

Useful articles