Computed properties
Describe a value that is worked out from other values with computed(), filter lists with it, and let Vue cache the result.
What you'll learn
When you finish, you can do these.- 1Work out a cart total from two refs with computed()
- 2Explain when a computed runs again or reuses its result
- 3Filter a list with a computed and loop over the result
- 4Spot and fix sorting in place and writing to a computed
Picture a cart on a shop page: a price, a quantity, and the total. The total comes from the price and the quantity. Kept as a separate value, it must be updated by hand after every change, and one forgotten update shows a wrong total.
computed(), a function from Vue, lets you describe such a value once, as a formula, and Vue keeps it right. The Code tab holds that cart, in the file App.vue, and a bowl of fruit you can narrow down to the ripe ones. By the end you can add a discount to the cart and a search box to the bowl. You need one idea from the Reactivity basics lesson: a ref, a box that holds a value and tells Vue when it changes.
Describe the value once
The cart total in App.vue is written as a computed. You give computed() a function, called the getter, that reads refs and returns a result: here an arrow function, () => followed by the value it returns. You get back a ref, read with .value in script and by name in the template.
The script block holds the two inputs, the price and the quantity, as refs and works out two values from them:
<script setup lang="ts">
import { ref, computed } from 'vue'
const price = ref(12)
const quantity = ref(2)
const total = computed(() => price.value * quantity.value)
const label = computed(() => (total.value > 50 ? 'Big order' : 'Small order'))
</script>
total is 12 times 2, so 24 euros at the start. label reads total, since a computed can read another computed, and says Big order past 50, Small order otherwise.
The template prints both by name, next to two buttons that change the inputs (@click runs its code on a click):
<template>
<p>{{ quantity }} × {{ price }} euros = {{ total }} euros. {{ label }}.</p>
<button @click="quantity++">Add one</button>
<button @click="price += 5">Raise the price</button>
</template>
The page reads "2 × 12 euros = 24 euros. Small order." Click Add one three times and it reads "5 × 12 euros = 60 euros. Big order."
Writing {{ price * quantity }} in the template, or calling a function you write, such as {{ getTotal() }}, gives the same number. The computed gives the value a name you can reuse, and it is cached, which the next section explains.
Cached until an input changes
A computed does not run its getter on every read: it saves the result.
The first time something reads total.value, Vue runs the getter and remembers the result and which refs it read. Noting which refs code reads is called tracking: it is also how a ref knows what to redraw (Reactivity basics). Later reads return the saved result without running the getter again: that is caching.
When price or quantity changes, Vue marks total out of date, but runs the getter only when something reads total again. This is called lazy evaluation: a computed that nothing reads costs nothing.
Show {{ total }} in three places and the getter still runs once per change. A function call such as {{ getTotal() }} runs on every render, each time Vue redraws the component, once per place.
Filtering a list
The most common computed in real apps is a list worked out from another list, such as the products that match a search.
A cart total is one number, so App.vue has a second part for lists: a bowl of three fruits and a Ripe only button. The ref fruits holds the list, and each fruit has a ripe flag, true or false. The ref ripeOnly says whether to filter, and the computed shown returns only the ripe fruit while ripeOnly is true, otherwise the whole list:
<script setup lang="ts">
const fruits = ref([
{ id: 1, name: 'Mango', ripe: true },
{ id: 2, name: 'Banana', ripe: false },
{ id: 3, name: 'Guava', ripe: true },
])
const ripeOnly = ref(false)
const shown = computed(() => {
const list = ripeOnly.value ? fruits.value.filter(f => f.ripe) : fruits.value
return list
})
</script>
filter() returns a new array holding only the items that pass its test, here f.ripe, and leaves fruits alone.
The template loops over shown with v-for, which repeats an element for each item of a list (the Directives lesson), and the button flips ripeOnly:
<template>
<button @click="ripeOnly = !ripeOnly">{{ ripeOnly ? 'Show all' : 'Ripe only' }}</button>
<ul>
<li v-for="fruit in shown" :key="fruit.id">{{ fruit.name }}</li>
</ul>
</template>
The page lists Mango, Banana and Guava under the Ripe only button. Click it: Banana goes, and the button says Show all.
Without a computed, the ripe test would sit in the template, on every row of the loop. The computed names the decision in script, and the loop goes over exactly the items to show.
What goes wrong
Three mistakes make a computed go wrong, and Vue warns about only one of them.
The first is sorting in place. Say you add a computed sorted that puts the fruit in order by name with sort(), and show it on the page. sort() does not make a new array: it reorders the one you call it on, here fruits itself.
The starter page lists the bowl on one line: "In the bowl: Mango, Banana, Guava". With sorted on the page, that line reads Banana, Guava, Mango, and so does the list under it, with no warning.
const sorted = computed(() => fruits.value.sort((a, b) => a.name.localeCompare(b.name)))
const sorted = computed(() => fruits.value.toSorted((a, b) => a.name.localeCompare(b.name)))
toSorted() returns a sorted copy and leaves fruits alone. A getter should only read and return: writing to a ref, starting a request or setting a timer inside one makes the page depend on when Vue runs it.
The second is assigning to a computed. Picture a Clear cart button whose function, clearCart, sets the total to 0. Nothing changes on the page, and the preview shows the warning Write operation failed: computed value is readonly.
function clearCart() { total.value = 0 }
function clearCart() { quantity.value = 0 }
A computed is read-only, so change an input: with the quantity at 0 the page reads "0 × 12 euros = 0 euros. Small order."
The third is reading something Vue cannot track. Picture a greeting that says Good morning before noon and Good afternoon after. new Date() and Date.now() read the clock, which is not a ref, so the getter runs once and the greeting never changes.
const greeting = computed(() => (new Date().getHours() < 12 ? 'Good morning' : 'Good afternoon'))
const now = ref(new Date())
setInterval(() => (now.value = new Date()), 60000)
const greeting = computed(() => (now.value.getHours() < 12 ? 'Good morning' : 'Good afternoon'))
The ref now gives Vue something to track, and setInterval, the browser's repeating timer, updates it every minute. The Lifecycle and template refs lesson shows how to stop that timer when the component goes away.
What to remember
computed(() => ...)describes a value worked out from other reactive values and returns a read-only ref.- Vue caches the result and runs the getter again only after something it read has changed, and only when the value is read.
- A filtered or sorted list is the classic computed, and
v-forloops over the result. Use methods that return a new array, such asfilter()andtoSorted(). - Keep getters pure: read, work out, return. Never sort in place, write to a ref or start a timer inside one.
- To change a computed, change its inputs. A computed that reads only non-reactive things such as
Date.now()never updates.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Add
const discount = ref(5)andconst finalTotal = computed(() => total.value - discount.value), and show<p>Pay {{ finalTotal }} euros</p>. With the starter numbers it reads "Pay 19 euros". Click Add one and it reads "Pay 31 euros". - Add
const query = ref('')in script and<input v-model="query" placeholder="Search" />right before the<ul>in the template.v-modelkeeps the input and the ref in step; the Forms and inputs lesson covers it. Inshown, returnlist.filter(f => f.name.toLowerCase().includes(query.value.toLowerCase())). Type "an": only Mango and Banana stay. - Add
const sorted = computed(() => fruits.value.sort((a, b) => a.name.localeCompare(b.name)))and<p>Sorted: {{ sorted.map(f => f.name).join(', ') }}</p>. The "In the bowl" line and the list under it now read Banana, Guava, Mango as well. ChangesorttotoSorted: the bowl goes back to Mango, Banana, Guava, and the sorted line stays sorted.