Vue.js course Β· Module 4: Computed & Watch

Computed Properties - Automatic Calculations

7 min read
In this lesson5

Welcome to the NOVA LAB Computation Center! The duty navigator has two numbers on the panel: the ship's velocity and the flight time. The third one, the distance traveled, gets recalculated by hand after every course correction. One forgotten update is enough for the panel to show a distance from an hour ago, and nobody notices. Dr. Nova has a simple rule for this: a value that can be derived from other data should calculate itself. In Vue, that job belongs to computed properties.

What you will learn

  • computed - derived values that recalculate on their own and remember the result.
  • computed with a getter and a setter - two-way fields, such as an energy unit converter.
  • watch - reacting to changes: a log entry, a server request, an alarm.
  • watchEffect - a watcher that works out by itself which data it uses.
  • stopping watchers and cleanup - closing telemetry channels once they are no longer needed.
  • practice and a project - a mission dashboard that combines all of these tools.

What are Computed Properties?

You import the computed function from the vue package, just like ref. You pass it a getter, an arrow function that returns the result of a calculation. In return you get an object similar to a ref: in the script you read its result through .value, and in the template simply by its name. Below is a navigation panel with two inputs stored in refs and one computed that calculates the distance.

1<template>
2  <div class="navigation-panel" style="background: #0a0e27; color: #00ff88;">
3    <p>Velocity: {{ velocity }} km/s</p>
4    <p>Time: {{ timeElapsed }} s</p>
5    <p>Distance: {{ distanceTraveled }} km</p>
6  </div>
7</template>
8
9<script setup>
10import { ref, computed } from 'vue'
11
12const velocity = ref(25)
13const timeElapsed = ref(3600)
14
15// Computed - automatically recalculates distance
16const distanceTraveled = computed(() => {
17  return velocity.value * timeElapsed.value
18})
19</script>

The getter multiplies velocity.value by timeElapsed.value, and Vue remembers which reactive values it read along the way. Those are the computed's dependencies. When you change the velocity from 25 to 30, the distance paragraph shows 108000 without a single extra line of code. Notice what did not change: velocity and timeElapsed are still ordinary refs, and the computed never writes to them, it only reads them. You cannot assign a value to the computed itself either - by default it is read-only, and an attempt to write ends with a warning in the console. The pattern worth memorizing always looks like this: const total = computed(() => a.value + b.value).

Computed vs Methods

You could produce the same text with a plain function called in the template. Both routes display identical text on the panel, so the difference lies elsewhere: in when the code runs. Below is a fuel indicator built both ways, with a console.log in each so you can see every run.

1<template>
2  <div class="fuel-display" style="background: #0a0e27; color: #00ff88;">
3    <!-- Computed - called without () -->
4    <p>{{ fuelStatus }}</p>
5
6    <!-- Method - called with () -->
7    <p>{{ getFuelStatus() }}</p>
8  </div>
9</template>
10
11<script setup>
12import { ref, computed } from 'vue'
13
14const fuelLevel = ref(750)
15const maxFuel = ref(1000)
16
17// Computed - cached, recalculated only when dependencies change
18const fuelStatus = computed(() => {
19  console.log('Computed executed')
20  const percentage = (fuelLevel.value / maxFuel.value) * 100
21  return `Fuel: ${percentage.toFixed(1)}%`
22})
23
24// Method - executed on every render
25function getFuelStatus() {
26  console.log('Method executed')
27  const percentage = (fuelLevel.value / maxFuel.value) * 100
28  return `Fuel: ${percentage.toFixed(1)}%`
29}
30</script>

In the template you write a computed without parentheses, because it is a value, and a method with parentheses, because it is a call. A method runs on every render of the component, even when the render was caused by a change in a completely unrelated field. A computed runs its getter only when fuelLevel or maxFuel changes, and on every other render it hands back the remembered result. Let us bust three myths while we are here. A computed is not "always faster" - the whole gain comes from remembering the result. A computed takes no arguments - when you need a parameter, reach for a method. And a computed does not work asynchronously: the getter must return a value right away, so await and server requests have no place in it.

Computed Caching

The remembered result is the cache. It pays off most with expensive calculations, such as averaging hundreds of sensor readings. In the example, the array method reduce walks through the readings and adds them up into one number, which we then divide by the number of readings.

1<script setup>
2import { ref, computed } from 'vue'
3
4const sensorReadings = ref([25, 30, 28, 32, 29])
5
6// Expensive operation - but cached!
7const averageTemperature = computed(() => {
8  console.log('Calculating average temperature...')
9  return sensorReadings.value.reduce((sum, reading) => {
10    // Simulating expensive operation
11    return sum + reading
12  }, 0) / sensorReadings.value.length
13})
14
15// Multiple references - only one calculation
16// {{ averageTemperature }}
17// {{ averageTemperature }}
18// {{ averageTemperature }}
19</script>

The three comments with {{ averageTemperature }} stand in for three places in the template that display the average. Despite three references, the message about calculating the average appears in the console once. And what happens when a new reading arrives in sensorReadings? Vue does not recalculate the average immediately. First it marks the computed as stale - in the Vue source code this flag is called "dirty". The getter runs only on the next read of the value, and the new result goes into the cache and is returned. If nobody reads the average, the getter does not run at all. Remember as well that only reactive data can be a dependency: a computed based on Date.now() would never refresh.

Filtering the module list

A typical job for a computed is a list filtered by whatever the operator typed into the search box. The filter method keeps only the array elements that meet a condition, toLowerCase converts text to lowercase, and includes checks whether a name contains the search fragment.

1import { ref, computed } from 'vue'
2
3const searchQuery = ref('')
4const modules = ref([
5  { id: 1, name: 'Life Support System' },
6  { id: 2, name: 'Oxygen Recycler' }
7])
8
9const filteredModules = computed(() => {
10  const query = searchQuery.value.toLowerCase()
11  return modules.value.filter((mod) => mod.name.toLowerCase().includes(query))
12})

This computed has two dependencies, searchQuery and modules, so the list refreshes both after a letter is typed and after a new module is added. Because both sides are converted to lowercase, letter case stops mattering. The original array does not change, because filter always returns a new one, and an empty search box lets every module through, since every name contains the empty fragment.

I recommend a simple rule: anything that can be derived from other data belongs in a computed, not in a separate ref that you update by hand. A single source of truth means fewer places where an update can be forgotten. In the next lesson you will teach a computed to accept writes, and after that you will meet watch - the tool for moments when you need an action rather than a value.

Remember: a computed is the station's navigation computer - it recalculates the course only when one of the parameters changes, and between changes it reports the remembered result.

Code for this lesson: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB Control Center - Mission Statistics
5const missionDays = ref(127)
6const crewMembers = ref(8)
7const oxygenLevel = ref(87)
8const powerOutput = ref(2400)
9const waterReserves = ref(3200)
10
11// Computed properties - automatically recalculated
12const missionWeeks = computed(() => {
13  return Math.floor(missionDays.value / 7)
14})
15
16const oxygenPerPerson = computed(() => {
17  return (oxygenLevel.value / crewMembers.value).toFixed(1)
18})
19
20const powerPerPerson = computed(() => {
21  return Math.floor(powerOutput.value / crewMembers.value)
22})
23
24const systemStatus = computed(() => {
25  if (oxygenLevel.value > 80 && powerOutput.value > 2000) {
26    return 'OPTIMAL'
27  } else if (oxygenLevel.value > 60 && powerOutput.value > 1500) {
28    return 'CAUTION'
29  }
30  return 'CRITICAL'
31})
32
33const statusColor = computed(() => {
34  const colors = {
35    'OPTIMAL': '#00ff88',
36    'CAUTION': '#ffaa00',
37    'CRITICAL': '#ff0044'
38  }
39  return colors[systemStatus.value]
40})
41
42function adjustOxygen(delta) {
43  oxygenLevel.value = Math.max(0, Math.min(100, oxygenLevel.value + delta))
44}
45
46function adjustPower(delta) {
47  powerOutput.value = Math.max(0, powerOutput.value + delta)
48}
49</script>
50
51<template>
52  <div class="control-center">
53    <div class="header">
54      <h1>NOVA LAB Control Center</h1>
55      <div class="mission-time">Sol {{ missionDays }} (Week {{ missionWeeks }})</div>
56    </div>
57
58    <div class="status-panel" :style="{ borderColor: statusColor }">
59      <h2>System Status: <span :style="{ color: statusColor }">{{ systemStatus }}</span></h2>
60      <div class="crew-info">Crew: {{ crewMembers }} members</div>
61    </div>
62
63    <div class="stats-grid">
64      <div class="stat-card">
65        <div class="stat-label">Oxygen Level</div>
66        <div class="stat-value">{{ oxygenLevel }}%</div>
67        <div class="stat-computed">{{ oxygenPerPerson }}% per person</div>
68        <div class="controls">
69          <button @click="adjustOxygen(-5)">-5%</button>
70          <button @click="adjustOxygen(5)">+5%</button>
71        </div>
72      </div>
73
74      <div class="stat-card">
75        <div class="stat-label">Power Output</div>
76        <div class="stat-value">{{ powerOutput }}W</div>
77        <div class="stat-computed">{{ powerPerPerson }}W per person</div>
78        <div class="controls">
79          <button @click="adjustPower(-100)">-100W</button>
80          <button @click="adjustPower(100)">+100W</button>
81        </div>
82      </div>
83
84      <div class="stat-card">
85        <div class="stat-label">Water Reserves</div>
86        <div class="stat-value">{{ waterReserves }}L</div>
87        <div class="stat-computed">{{ (waterReserves / crewMembers).toFixed(1) }}L per person</div>
88      </div>
89    </div>
90
91    <div class="info-box">
92      <strong>Computed Properties:</strong> Values automatically update when dependencies change!
93    </div>
94  </div>
95</template>
96
97<style>
98* {
99  margin: 0;
100  padding: 0;
101  box-sizing: border-box;
102}
103
104body {
105  font-family: 'Courier New', monospace;
106  background: #0a0e27;
107  color: #00ff88;
108  padding: 20px;
109}
110
111.control-center {
112  max-width: 900px;
113  margin: 0 auto;
114}
115
116.header {
117  text-align: center;
118  margin-bottom: 30px;
119  padding: 20px;
120  background: linear-gradient(135deg, #1a1f4c 0%, #0a0e27 100%);
121  border: 2px solid #00ff88;
122  border-radius: 10px;
123}
124
125.header h1 {
126  font-size: 2rem;
127  margin-bottom: 10px;
128  text-shadow: 0 0 20px #00ff88;
129}
130
131.mission-time {
132  color: #00b4d8;
133  font-size: 1.2rem;
134}
135
136.status-panel {
137  padding: 20px;
138  margin-bottom: 30px;
139  background: rgba(0, 255, 136, 0.05);
140  border: 2px solid;
141  border-radius: 10px;
142  text-align: center;
143  transition: all 0.3s ease;
144}
145
146.status-panel h2 {
147  font-size: 1.5rem;
148  margin-bottom: 10px;
149}
150
151.crew-info {
152  color: #00b4d8;
153  font-size: 1.1rem;
154}
155
156.stats-grid {
157  display: grid;
158  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
159  gap: 20px;
160  margin-bottom: 30px;
161}
162
163.stat-card {
164  background: rgba(0, 180, 216, 0.05);
165  border: 2px solid #00b4d8;
166  border-radius: 10px;
167  padding: 20px;
168  text-align: center;
169}
170
171.stat-label {
172  color: #00b4d8;
173  font-size: 0.9rem;
174  margin-bottom: 10px;
175  text-transform: uppercase;
176}
177
178.stat-value {
179  font-size: 2.5rem;
180  font-weight: bold;
181  color: #00ff88;
182  margin-bottom: 10px;
183  text-shadow: 0 0 10px #00ff88;
184}
185
186.stat-computed {
187  color: #ffaa00;
188  font-size: 1rem;
189  margin-bottom: 15px;
190  font-style: italic;
191}
192
193.controls {
194  display: flex;
195  gap: 10px;
196  justify-content: center;
197}
198
199button {
200  background: linear-gradient(135deg, #00b4d8 0%, #0077aa 100%);
201  color: white;
202  border: none;
203  padding: 10px 20px;
204  border-radius: 5px;
205  cursor: pointer;
206  font-family: 'Courier New', monospace;
207  font-weight: bold;
208  transition: all 0.3s ease;
209}
210
211button:hover {
212  background: linear-gradient(135deg, #00ff88 0%, #00cc66 100%);
213  transform: translateY(-2px);
214  box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
215}
216
217button:active {
218  transform: translateY(0);
219}
220
221.info-box {
222  background: rgba(0, 255, 136, 0.1);
223  border: 2px solid #00ff88;
224  border-radius: 10px;
225  padding: 15px;
226  text-align: center;
227  color: #00ff88;
228}
229</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 a computed property in Vue.js?

  2. 2. What is the main advantage of computed properties over regular methods?

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

Hands-on tasks in the game

  • Code editor

    Complete the code - create a computed property that calculates distance based on speed and time.

  • Click in order

    Arrange the correct syntax for creating a computed property:

  • Code editor

    Complete the computed property that filters a list of modules based on the entered query.

  • Vertical ordering

    Arrange the stages of computed property operation from data change to result:

Useful articles