Vue.js 21 🟢 Deriving State with computed() Getters and Setters
A computed property is a derived value. It reads from other reactive values and produces a new one. The template, the script, and the other computeds use it as if it were a plain value, and Vue ensures that it updates when its dependencies change. The computed() function is the Composition API’s mechanism for this. It takes a getter function and returns a read-only ref. With a setter, it returns a writable ref, and the setter becomes the way the derived value is written.
Key point: computed() returns a ref. When the ref is read, the getter runs and the result is cached. When a dependency changes, the cache is invalidated, and the next read recomputes. The getter should be pure—no side effects, no assignments—because Vue may run it multiple times and the result is expected to be deterministic. The setter form, computed({ get, set }), returns a writable ref and is the pattern for the two-way bindings that transform the value.
Why computed exists
The derived-state problem. A value that is computed from other values should not be stored separately. If it is stored, it must be updated whenever the source changes, and the update is easy to forget. A computed derives the value on demand, and the derivation is the single source of truth. There is no separate state to keep in sync.
The template-recomputation problem. A method or an inline expression in the template runs on every render. The {{ items.filter(i => i.active).length }} recomputes on every re-render, even when the items have not changed. A computed caches the result and recomputes only when a dependency changes. The cache is the performance benefit.
The purity problem. A computed should be pure. It reads the reactive dependencies and returns a value. It should not assign to other state, make network requests, or mutate the arguments. The purity is what makes the cache valid: the same dependencies produce the same result.
The two-way problem. Sometimes the derived value needs to be written. A full name that is computed from the first and last names, and a setter that splits the input and assigns the parts. The computed({ get, set }) form makes the derived value writable, and the setter is the transformation in the opposite direction.
The reactive-in-template problem. A computed is a ref, and the template auto-unwraps it. The {{ fullName }} reads the computed’s value, and the reactivity is preserved. The computed can be passed to a child as a prop, and the child re-renders when the computed changes.
The composition problem. A computed can read other computeds. The chain of the derived values is the pipeline, and the cache is per computed. The chain recomputes only the computeds whose dependencies changed.
a. The read-only computed
A read-only computed is created with a getter function.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
<template>
<p>{{ fullName }}</p>
</template>
The fullName is a read-only ref. The template auto-unwraps it. The script reads it with .value.
<script setup>
const { firstName, lastName, fullName } = useName()
console.log(fullName.value) // 'Alice Smith'
firstName.value = 'Bob'
console.log(fullName.value) // 'Bob Smith'
The computed recomputes when the firstName or the lastName changes. The recomputation is lazy: the next read of the fullName triggers the getter, and the result is cached until a dependency changes again.
The computed can be used in another computed.
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ name: 'Apple', price: 1.0, quantity: 3 },
{ name: 'Banana', price: 0.5, quantity: 6 },
])
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const tax = computed(() => subtotal.value * 0.1)
const total = computed(() => subtotal.value + tax.value)
</script>
The subtotal, the tax, and the total are the chain. The tax reads the subtotal, and the total reads the subtotal and the tax. The change to the items invalidates the subtotal, which invalidates the tax and the total. The recomputation is the chain.
The computed that does not depend on any reactive value never updates.
<script setup>
const now = computed(() => Date.now())
</script>
The now computes once and never changes. The Date.now() is not a reactive dependency. Use ref with an interval if the value should update.
The computed that has a side effect is a bug.
<script setup>
const doubled = computed(() => {
console.log('computing') // side effect
return count.value * 2
})
</script>
The console.log runs on every recomputation, and the recomputation may happen more than once. The getter should be pure. The side effects belong in a watch or an event handler.
b. The writable computed
A writable computed is created with an object that has a get and a set.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})
</script>
<template>
<input v-model="fullName">
</template>
The v-model on the input reads the fullName and writes to it. The read calls the getter. The write calls the setter with the new value. The setter splits the input and assigns the parts. The firstName and the lastName update, and the fullName recomputes.
The writable computed is the pattern for the two-way bindings that transform. The v-model on the input is two-way, and the computed’s getter and setter are the two directions.
The setter can validate or normalize the value.
<script setup>
const raw = ref('')
const normalized = computed({
get() {
return raw.value.trim().toLowerCase()
},
set(value) {
raw.value = value.replace(/\s+/g, ' ')
}
})
</script>
The getter returns the trimmed and lowercased value. The setter collapses the whitespace. The raw and the normalized stay in sync.
The setter can be used with the multiple sources.
<script setup>
const celsius = ref(0)
const fahrenheit = computed({
get() {
return celsius.value * 9 / 5 + 32
},
set(value) {
celsius.value = (value - 32) * 5 / 9
}
})
</script>
<template>
<input v-model.number="celsius"> °C
<input v-model.number="fahrenheit"> °F
</template>
The celsius and the fahrenheit are two views of the same temperature. The edit to either input updates the other.
The writable computed can be used in the v-model on a custom component.
<script setup>
const model = defineModel()
const formatted = computed({
get: () => model.value.toUpperCase(),
set: (value) => { model.value = value.toLowerCase() },
})
</script>
The formatted is the derived view of the model. The parent writes the lowercase, and the child displays the uppercase.
c. The caching and the debugging
The computed is cached. The getter runs on the first read and caches the result. The subsequent reads return the cached value until a dependency changes.
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => {
console.log('computing doubled')
return count.value * 2
})
console.log(doubled.value) // 'computing doubled', 0
console.log(doubled.value) // 0 (cached, no log)
count.value = 1
console.log(doubled.value) // 'computing doubled', 2
The log appears on the first read and on the read after the change. The second read in the same state does not log because the value is cached.
The caching is the difference from a method.
<script setup>
function doubledFn() {
console.log('computing')
return count.value * 2
}
</script>
<template>
<p>{{ doubled }}</p> <!-- cached -->
<p>{{ doubledFn() }}</p> <!-- runs on every render -->
</template>
The method runs on every render. The computed runs only when the dependency changes. The computed is the right choice for the derived values.
The debugging of the computed uses the onTrack and onTrigger options.
<script setup>
const doubled = computed(() => count.value * 2, {
onTrack(e) {
console.log('tracked', e)
},
onTrigger(e) {
console.log('triggered', e)
},
})
</script>
The onTrack runs when the reactive dependency is tracked. The onTrigger runs when the dependency triggers the recomputation. The options are for the debugging, not for the production.
The computed can be compared with the watch.
| Aspect | computed | watch |
|---|---|---|
| Returns | Ref | Nothing |
| Purpose | Derive a value | Run a side effect |
| Cache | Yes | N/A |
| Lazy | Yes | No (eager by default) |
| Side effects | Should be pure | The point |
The computed derives a value. The watch runs a side effect when a value changes. The two are different tools. The computed for the derived state, the watch for the side effects.
Complete Example Session
<!-- ============================================ -->
<!-- PART 1: BASIC COMPUTED -->
<!-- ============================================ -->
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
<template>
<p>{{ count }} × 2 = {{ doubled }}</p>
</template>
<!-- ============================================ -->
<!-- PART 2: COMPUTED FROM MULTIPLE REFS -->
<!-- ============================================ -->
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
<!-- ============================================ -->
<!-- PART 3: COMPUTED CHAIN -->
<!-- ============================================ -->
<script setup>
const items = ref([{ price: 10, qty: 2 }, { price: 5, qty: 4 }])
const subtotal = computed(() => items.value.reduce((s, i) => s + i.price * i.qty, 0))
const tax = computed(() => subtotal.value * 0.1)
const total = computed(() => subtotal.value + tax.value)
</script>
<!-- ============================================ -->
<!-- PART 4: WRITABLE COMPUTED -->
<!-- ============================================ -->
<script setup>
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})
</script>
<!-- ============================================ -->
<!-- PART 5: WRITABLE COMPUTED WITH V-MODEL -->
<!-- ============================================ -->
<template>
<input v-model="fullName">
</template>
<!-- ============================================ -->
<!-- PART 6: TEMPERATURE CONVERSION -->
<!-- ============================================ -->
<script setup>
const celsius = ref(0)
const fahrenheit = computed({
get: () => celsius.value * 9 / 5 + 32,
set: (value) => { celsius.value = (value - 32) * 5 / 9 }
})
</script>
<template>
<input v-model.number="celsius"> °C
<input v-model.number="fahrenheit"> °F
</template>
<!-- ============================================ -->
<!-- PART 7: NORMALIZING SETTER -->
<!-- ============================================ -->
<script setup>
const raw = ref('')
const normalized = computed({
get: () => raw.value.trim().toLowerCase(),
set: (value) => { raw.value = value.replace(/\s+/g, ' ') }
})
</script>
<!-- ============================================ -->
<!-- PART 8: COMPUTED VS METHOD -->
<!-- ============================================ -->
<script setup>
const count = ref(0)
const doubled = computed(() => count.value * 2)
function doubledFn() {
return count.value * 2
}
</script>
<template>
<p>{{ doubled }}</p> <!-- cached -->
<p>{{ doubledFn() }}</p> <!-- runs every render -->
</template>
<!-- ============================================ -->
<!-- PART 9: COMPUTED WITH DEBUG -->
<!-- ============================================ -->
<script setup>
const doubled = computed(() => count.value * 2, {
onTrack: (e) => console.log('tracked', e.key),
onTrigger: (e) => console.log('triggered', e.key),
})
</script>
<!-- ============================================ -->
<!-- PART 10: FILTERED LIST -->
<!-- ============================================ -->
<script setup>
import { ref, computed } from 'vue'
const search = ref('')
const items = ref([
{ name: 'Apple' },
{ name: 'Banana' },
{ name: 'Cherry' },
])
const filtered = computed(() =>
items.value.filter((i) =>
i.name.toLowerCase().includes(search.value.toLowerCase())
)
)
</script>
<template>
<input v-model="search">
<ul>
<li v-for="item in filtered" :key="item.name">{{ item.name }}</li>
</ul>
</template>
The ten parts covered the basic computed, the computed from the multiple refs, the computed chain, the writable computed, the writable computed with v-model, the temperature conversion, the normalizing setter, the computed vs the method, the computed with the debug, and the filtered list.
Quick Reference
Computed Forms
| Form | Returns |
|---|---|
computed(getter) | Read-only ref |
computed({ get, set }) | Writable ref |
computed(getter, { onTrack, onTrigger }) | Read-only with debug |
Access
| Context | Access |
|---|---|
| Script | .value |
| Template | Auto-unwrapped |
| Nested | .value |
Computed vs Method
| Aspect | Computed | Method |
|---|---|---|
| Caching | Yes | No |
| Runs | On dependency change | On every call |
| Template | {{ c }} | {{ m() }} |
| Use | Derived value | Event handler, side effect |
Computed vs Watch
| Aspect | Computed | Watch |
|---|---|---|
| Returns | Ref | Nothing |
| Purpose | Derive | Side effect |
| Lazy | Yes | No |
| Cached | Yes | No |
Writable Computed
| Part | Purpose |
|---|---|
get | Read the derived value |
set | Write the source values |
v-model | Two-way with the computed |
Best Practices
✅ Do This:
<!-- Use computed for derived values -->
const fullName = computed(() => `${first.value} ${last.value}`) // ✅
<!-- Use the writable form for the two-way transformations -->
const fullName = computed({
get: () => `${first.value} ${last.value}`,
set: (v) => { [first.value, last.value] = v.split(' ') }
}) // ✅
<!-- Use the computed chain -->
const total = computed(() => subtotal.value + tax.value) // ✅
<!-- Use the computed for the filtered lists -->
const active = computed(() => items.value.filter(i => i.active)) // ✅
<!-- Keep the getter pure -->
const doubled = computed(() => count.value * 2) // ✅
❌ Don’t Do This:
<!-- Don't use a method for the derived value -->
function doubled() { return count.value * 2 } // ⚠️
<!-- Don't put the side effects in the getter -->
const doubled = computed(() => {
console.log('computing') // side effect // ❌
return count.value * 2
})
<!-- Don't assign to the computed directly -->
const doubled = computed(() => count.value * 2)
doubled.value = 10 // error // ❌
<!-- Don't use the computed for the side effects -->
const log = computed(() => {
console.log(count.value) // use watch // ❌
})
<!-- Don't forget the .value in the script -->
console.log(doubled) // ref object // ⚠️
<!-- Don't create a computed that never updates -->
const now = computed(() => Date.now()) // no dependency // ⚠️
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Computed not updating | No reactive dependency | Check the dependency |
| Side effect in getter | Wrong tool | Use watch |
doubled.value = 10 | Read-only | Use the writable form |
| Method used instead | No caching | Use computed |
Date.now() in computed | Not reactive | Use ref + interval |
| Computed not unwrapped | Missing .value | Use .value in script |
Real-World Examples
1. Full Name
const fullName = computed(() => `${first.value} ${last.value}`)
2. Filtered List
const active = computed(() => items.value.filter(i => i.active))
3. Total Price
const total = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0)
)
4. Writable Full Name
const fullName = computed({
get: () => `${first.value} ${last.value}`,
set: (v) => { [first.value, last.value] = v.split(' ') }
})
5. Temperature
const fahrenheit = computed({
get: () => celsius.value * 9 / 5 + 32,
set: (v) => { celsius.value = (v - 32) * 5 / 9 }
})
6. Normalized Input
const normalized = computed({
get: () => raw.value.trim().toLowerCase(),
set: (v) => { raw.value = v.replace(/\s+/g, ' ') }
})
7. Sorted List
const sorted = computed(() => [...items.value].sort((a, b) => a.name.localeCompare(b.name)))
8. Count Active
const activeCount = computed(() => items.value.filter(i => i.active).length)
9. Is Valid
const isValid = computed(() => email.value.includes('@') && password.value.length >= 8)
10. Formatted Currency
const formatted = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(total.value)
)
Visual
Computed Caching
┌─────────────────────────────────────────────────────────────┐
│ const count = ref(0) │
│ const doubled = computed(() => count.value * 2) │
│ │
│ Read doubled.value │
│ │ │
│ ▼ │
│ Cache empty? ──▶ Run getter ──▶ Cache the result │
│ │ │
│ ▼ │
│ Read doubled.value │
│ │ │
│ ▼ │
│ Cache valid? ──▶ Return the cached value (no getter) │
│ │ │
│ ▼ │
│ count.value = 1 │
│ │ │
│ ▼ │
│ Cache invalidated │
│ │ │
│ ▼ │
│ Read doubled.value ──▶ Run getter ──▶ Cache the new value │
│ │
└─────────────────────────────────────────────────────────────┘
Computed Chain
┌─────────────────────────────────────────────────────────────┐
│ items │
│ │ │
│ ▼ │
│ subtotal = computed(...) │
│ │ │
│ ├──▶ tax = computed(() => subtotal * 0.1) │
│ │ │
│ └──▶ total = computed(() => subtotal + tax) │
│ │
│ The change to items invalidates the chain. │
│ The recomputation is lazy and cached. │
│ │
└─────────────────────────────────────────────────────────────┘
Writable Computed
┌─────────────────────────────────────────────────────────────┐
│ computed({ get, set }) │
│ │
│ READ │
│ │ │
│ ▼ │
│ get() ──▶ Derived value │
│ │
│ WRITE │
│ │ │
│ ▼ │
│ set(value) ──▶ Update the sources │
│ │
│ The getter and the setter are the two directions. │
│ │
└─────────────────────────────────────────────────────────────┘
Computed vs Method
┌─────────────────────────────────────────────────────────────┐
│ COMPUTED │
│ │
│ {{ doubled }} │
│ Runs: on the dependency change │
│ Cached: yes │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ METHOD │
│ │
│ {{ doubledFn() }} │
│ Runs: on every render │
│ Cached: no │
│ │
│ Use the computed for the derived values. │
│ Use the method for the event handlers. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Function | computed(getter) |
| Returns | Read-only ref |
| Writable | computed({ get, set }) |
| Access (script) | .value |
| Access (template) | Auto-unwrapped |
| Cache | Yes |
| Lazy | Yes |
| Chain | Yes |
| Debug | onTrack, onTrigger |
| vs method | Cached vs uncached |
| vs watch | Derive vs side effect |
Key takeaways:
computed()returns a ref. The getter runs on the first read and caches the result. The subsequent reads return the cached value until a dependency changes. The template auto-unwraps the ref .- The getter should be pure. It reads the reactive dependencies and returns a value. It should not assign to the other state, make the network requests, or mutate the arguments. The purity is what makes the cache valid.
- The computed can chain. A computed can read another computed. The change to the source invalidates the chain, and the recomputation is lazy. The chain is the pipeline of the derived values .
- The writable computed is
computed({ get, set }). The getter reads the derived value, and the setter writes the source values. Thev-modelon the computed uses both. The pattern is for the two-way transformations . - The computed caches; the method does not. The method runs on every render. The computed runs only when the dependency changes. The computed is the right choice for the derived values, and the method is the right choice for the event handlers .
- The computed is not the side effect. The
watchis the side effect. Thecomputedderives a value. The two are different tools, and using the computed for the side effects is a bug . - The debugging options are
onTrackandonTrigger. They run when the dependency is tracked and when it triggers the recomputation. They are for the debugging, not for the production .
Remember: The computed is the derived state. It reads the reactive values and produces a new one, and the caching makes it efficient. Use it for the full name, the filtered list, the total price, the validation flag. Use the writable form for the two-way transformations: the full name that splits, the temperature that converts, the normalized input. And keep the getter pure: the derived value is the result, not the side effect. The side effects belong in the watch, and the events belong in the methods. The computed is the value, and the value is what the template renders.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!