Vue.js course ยท Module 4: Computed & Watch

Project: Mission Dashboard with Computed and Watch

5 min read
In this lesson3

In three days NOVA LAB sends a transport toward Valles Marineris. Today the operators calculate the arrival time on paper, report engine temperature alarms over the radio, and assemble the report for command by hand at the end of the shift. Dr. Nova wants a single panel that does all of this by itself: it calculates the flight parameters, keeps an eye on critical systems and writes the report.

Final project of the Computation Center! Build an advanced space mission dashboard that calculates flight parameters in real time, monitors critical systems, and automatically generates reports. You'll use computed properties for automatic calculations and watchers for reactive change monitoring.

Module Summary

In this module you learned:

  • Computed properties - automatic calculations dependent on reactive data. Cached - they only recalculate when a dependency changes. Called without parentheses: {{ fuelStatus }}.
  • Computed with getter/setter - bidirectional computed properties. Getter reads, setter parses and splits data. Useful for unit conversion and format parsing.
  • watch() - manual change observation with full control. Explicit source, access to oldValue/newValue, options immediate, deep, once. Watch on multiple sources simultaneously.
  • watchEffect() - automatic dependency tracking. No need to declare what to watch - Vue detects it automatically. Executes immediately. Cleanup via onCleanup.
  • Stopping watchers - every watcher returns a stop function. Pausing/resuming in Vue 3.5+. Conditional stopping in callback.

What already works in the starter

The starter below the lesson is not a blank page. It keeps the mission state in refs: fuel, velocity, engine temperature, distance traveled and target distance, the monitoring flag and the log. Two computed values calculate the estimated arrival time and the percentage of the route completed, and the writable computed speedKmH converts velocity between km/s and km/h. The temperature watcher is created in the startMonitoring function, and stopMonitoring closes it with the saved handle. A watchEffect adds a low fuel warning to the log. The starter uses a simplified scale, degrees Celsius with thresholds of 400 and 500. The requirements below describe the target version in kelvins, so rescale the thresholds and add the missing parts.

Project Requirements

Build a Mission Control Dashboard that includes:

1. Computed properties for mission calculations

Input data: velocity (km/s), distance to target (km), fuel consumption (liters/s), tank capacity (liters). Automatically calculate: estimated arrival time, total fuel consumption, remaining fuel percentage, mission status (NOMINAL/WARNING/CRITICAL).

Derive the status with a single computed, checking the conditions from the most serious one: CRITICAL first, then WARNING, and NOMINAL last. The order of the conditions decides which state wins. The getter should only calculate, so no log entries inside it.

2. Watch on critical parameters

Monitor engine temperature (input range 0-5000K). When it exceeds 3500K, display a "WARNING". When it exceeds 4500K, display "CRITICAL" and automatically reduce velocity. Save temperature change history (last 10 values).

This is a job for watch, because the reaction is an action: a velocity change and a history entry. Keep the history as in the trajectory calculator, with unshift and pop. Lowering the velocity inside the callback will not cause a loop, because it changes a different source than the watched temperature.

3. watchEffect on system status

Automatically generate a text report in a side panel that updates with every change in mission parameters. The report should include current velocity, temperature, fuel consumption, and estimated arrival time.

watchEffect returns nothing, so write the report into a separate ref, for example reportText. If the report were only displayed, a computed would be enough. Here we are practicing an effect, so add the time the report was generated, and that is already an action, not a pure value.

4. Cleanup and watcher control

Add a "Monitoring ON/OFF" button that enables/disables the temperature watcher. When monitoring is off, temperature changes don't generate alarms. Use the stop() function and watcher re-creation.

The key is the handle returned by watch. Keep it in a variable outside the function, and when switching off, call it and reset it, so that the next switch-on creates a fresh watcher.

1let stopTempWatch = null
2
3function startMonitoring() {
4  stopTempWatch = watch(engineTemp, (newTemp) => {
5    // alarms and history
6  })
7}
8
9function stopMonitoring() {
10  if (stopTempWatch) {
11    stopTempWatch()
12    stopTempWatch = null
13  }
14}

After calling stopTempWatch() the temperature is still reactive and the panel keeps displaying it - only the alarm listener goes dark. Vue 3.5 also has pause() and resume(), but in this project we practice the full cycle: stopping the watcher and creating it again.

I recommend building the panel in stages: computed values and a check of the numbers first, then the alarms, and finally the report and the switch. In the next module, in the Control Simulator, you will learn events and forms, which will let the crew operate a panel like this one. Expand the starter below!

Remember: a good mission dashboard calculates through computed, reacts through watch, and every listening channel it opens, it can also close.

Code for this lesson: App.vue
1<script setup>
2import { ref, computed, watch, watchEffect } from 'vue'
3
4// Mission Control Dashboard - Module 4 final project
5const fuelLevel = ref(100)
6const speed = ref(0)
7const engineTemp = ref(200)
8const distance = ref(0)
9const targetDistance = ref(1000)
10const monitoringActive = ref(true)
11
12const logs = ref([])
13
14// Computed: estimated time of arrival
15const estimatedTime = computed(() => {
16  if (speed.value <= 0) return 'N/A'
17  const remaining = targetDistance.value - distance.value
18  return (remaining / speed.value).toFixed(1) + ' h'
19})
20
21// Computed: mission completion percentage
22const missionProgress = computed(() => {
23  return Math.min(100, (distance.value / targetDistance.value * 100)).toFixed(1)
24})
25
26// Computed with getter/setter: speed in km/s and km/h
27const speedKmH = computed({
28  get: () => speed.value * 3600,
29  set: (val) => { speed.value = val / 3600 }
30})
31
32// Watch on engine temperature
33let stopTempWatch = null
34function startMonitoring() {
35  monitoringActive.value = true
36  stopTempWatch = watch(engineTemp, (newTemp) => {
37    if (newTemp > 500) {
38      logs.value.unshift('ALARM: Critical temperature!')
39    } else if (newTemp > 400) {
40      logs.value.unshift('Warning: High engine temperature')
41    }
42  })
43}
44
45function stopMonitoring() {
46  if (stopTempWatch) {
47    stopTempWatch()
48    monitoringActive.value = false
49    logs.value.unshift('Monitoring stopped')
50  }
51}
52
53startMonitoring()
54
55// watchEffect: automatic report
56watchEffect(() => {
57  if (fuelLevel.value < 20) {
58    logs.value.unshift('REPORT: Low fuel level!')
59  }
60})
61
62function accelerate() { speed.value = Math.min(100, speed.value + 5); engineTemp.value += 15 }
63function decelerate() { speed.value = Math.max(0, speed.value - 5); engineTemp.value -= 10 }
64function travel() { distance.value += speed.value * 10; fuelLevel.value = Math.max(0, fuelLevel.value - 2) }
65</script>
66
67<template>
68  <div class="dashboard">
69    <h1>Mission Control Dashboard</h1>
70
71    <div class="stats">
72      <div class="stat"><span>Fuel</span><strong>{{ fuelLevel }}%</strong></div>
73      <div class="stat"><span>Speed</span><strong>{{ speed.toFixed(1) }} km/s</strong></div>
74      <div class="stat"><span>Temperature</span><strong>{{ engineTemp }}ยฐC</strong></div>
75      <div class="stat"><span>Distance</span><strong>{{ distance.toFixed(0) }} / {{ targetDistance }} km</strong></div>
76      <div class="stat"><span>Progress</span><strong>{{ missionProgress }}%</strong></div>
77      <div class="stat"><span>ETA</span><strong>{{ estimatedTime }}</strong></div>
78    </div>
79
80    <div class="controls">
81      <button @click="accelerate">Accelerate</button>
82      <button @click="decelerate">Decelerate</button>
83      <button @click="travel">Travel (+10h)</button>
84      <button @click="monitoringActive ? stopMonitoring() : startMonitoring()">
85        {{ monitoringActive ? 'Disable monitoring' : 'Enable monitoring' }}
86      </button>
87    </div>
88
89    <div class="logs" v-if="logs.length">
90      <h3>System logs</h3>
91      <p v-for="(log, i) in logs" :key="i">{{ log }}</p>
92    </div>
93  </div>
94</template>
95
96<style scoped>
97.dashboard { background: #0a0a2e; color: #e0e0ff; padding: 2rem; min-height: 100vh; font-family: monospace; }
98h1 { color: #00d4ff; text-align: center; }
99.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1rem 0; }
100.stat { background: #1a1a4e; padding: 1rem; border-radius: 8px; text-align: center; }
101.stat span { display: block; color: #888; font-size: 0.8rem; }
102.stat strong { font-size: 1.2rem; color: #00d4ff; }
103.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; margin: 1rem 0; }
104button { background: #333; color: white; border: none; padding: 0.5rem 1rem; cursor: pointer; border-radius: 4px; }
105button:hover { background: #555; }
106.logs { background: #111; padding: 1rem; border-radius: 8px; max-height: 200px; overflow-y: auto; }
107.logs h3 { color: #ffaa00; }
108.logs p { color: #aaa; font-size: 0.85rem; margin: 0.2rem 0; }
109</style>

Spotted a mistake in this lesson?

Useful articles