Vue.js course Β· Module 4: Computed & Watch
Computed Properties - Automatic Calculations
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. What is a computed property in Vue.js?
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: