Chapter 15: Vue Computed Properties
Vue Computed Properties in Vue 3 (the modern way everyone uses in 2026). This is one of the most important reactivity tools after ref / reactive, and once you really get it, your code becomes cleaner, faster, and easier to reason about.
What are Computed Properties? (Simple Teacher Explanation)
Imagine you have some raw data (like firstName and lastName refs). You want to show a full name = first + last, but:
- You don’t want to repeat the concatenation logic in 5 places in your template
- You want it to automatically update whenever firstName or lastName changes
- You don’t want it recalculated on every render (like a method would be)
Computed properties solve exactly this:
- They are lazy → only calculate when needed (first access or when dependencies change)
- They are cached → if dependencies didn’t change → return the old value instantly (super performant)
- They are reactive → template auto-updates when the computed value changes
- They feel like a derived data property (read-only by default)
In short:
- Methods → run every time you call them (good for actions: submit, toggle, API call)
- Computed → run only when dependencies change (good for derived values: fullName, filteredList, totalPrice)
Basic Example – Full Name (Classic Hello World)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<template> <div class="demo"> <input v-model="firstName" placeholder="First name" /> <input v-model="lastName" placeholder="Last name" /> <p>Full name: <strong>{{ fullName }}</strong></p> <p>Length: {{ fullNameLength }}</p> <button @click="reverseNames">Reverse Names</button> </div> </template> <script setup> import { ref, computed } from 'vue' const firstName = ref('Rahul') const lastName = ref('Sharma') // Computed – auto-updates when firstName or lastName changes const fullName = computed(() => { return `${firstName.value.trim()} ${lastName.value.trim()}`.trim() }) // Another computed that depends on the first one (chaining is fine!) const fullNameLength = computed(() => { return fullName.value.length }) function reverseNames() { const temp = firstName.value firstName.value = lastName.value lastName.value = temp } </script> <style scoped> .demo { padding: 2rem; max-width: 500px; margin: auto; } input { margin: 0.5rem 0; padding: 0.6rem; width: 100%; } p { margin: 1rem 0; font-size: 1.2rem; } </style> |
→ Type in either input → fullName updates instantly → No .value needed in template (auto-unwrapped) → fullNameLength only re-runs when fullName changes → efficient!
Why Not Just Use a Method?
Compare:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- This is BAD for performance if used many times --> <p>{{ getFullName() }}</p> <script setup> function getFullName() { console.log('Running getFullName') // ← will log on EVERY render! return `${firstName.value} ${lastName.value}` } </script> |
→ Every time Vue re-renders (e.g. parent component updates, even unrelated state change) → method runs again → waste!
Computed → only runs when firstName or lastName actually changes.
Real-World Example – Todo List Stats (Very Common Pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
<template> <div class="todo-app"> <input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo..." /> <ul> <li v-for="todo in filteredTodos" :key="todo.id"> <input type="checkbox" v-model="todo.done" /> <span :class="{ done: todo.done }">{{ todo.text }}</span> </li> </ul> <footer> <p>{{ remaining }} of {{ total }} remaining</p> <div class="filters"> <button v-for="f in filters" :key="f.value" :class="{ active: filter === f.value }" @click="filter = f.value" > {{ f.label }} </button> </div> </footer> </div> </template> <script setup> import { ref, computed } from 'vue' const newTodo = ref('') const todos = ref([ { id: 1, text: 'Learn Vue 3', done: true }, { id: 2, text: 'Build a real app', done: false }, { id: 3, text: 'Teach computed properties', done: false } ]) const filter = ref('all') const filteredTodos = computed(() => { if (filter.value === 'active') return todos.value.filter(t => !t.done) if (filter.value === 'completed') return todos.value.filter(t => t.done) return todos.value // 'all' }) const remaining = computed(() => todos.value.filter(t => !t.done).length) const total = computed(() => todos.value.length) function addTodo() { if (!newTodo.value.trim()) return todos.value.push({ id: Date.now(), text: newTodo.value.trim(), done: false }) newTodo.value = '' } const filters = [ { value: 'all', label: 'All' }, { value: 'active', label: 'Active' }, { value: 'completed',label: 'Completed' } ] </script> <style scoped> .done { text-decoration: line-through; color: #888; } .filters button { margin: 0 0.5rem; padding: 0.4rem 1rem; } .filters .active { background: #3b82f6; color: white; } </style> |
Key lessons here:
- filteredTodos → depends on todos + filter → re-computes only when they change
- remaining & total → cheap, cached calculations
- No performance hit even if template re-renders for other reasons
Writable Computed (Advanced – Get + Set)
Sometimes you want two-way binding on a computed value (rare but useful):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<script setup> import { ref, computed } from 'vue' const price = ref(100) const formattedPrice = computed({ get: () => `₹${price.value.toFixed(2)}`, set: (newVal) => { // Clean input like "₹123.45" → 123.45 const num = parseFloat(newVal.replace(/[^0-9.]/g, '')) if (!isNaN(num)) price.value = num } }) </script> <template> <input v-model="formattedPrice" placeholder="Enter price" /> <p>Raw price: {{ price }}</p> </template> |
→ Type “₹150.99” → price.value becomes 150.99
Quick Comparison Table – 2026 Vue 3
| Feature | Computed | Method | Watch |
|---|---|---|---|
| When runs | Only when dependencies change | Every time called | When source changes (side effects) |
| Cached? | Yes | No | No |
| Return value? | Yes (the result) | Yes (whatever you return) | No (side effects) |
| Use case | Derived data (fullName, filteredList) | Actions (submit, toggle) | Async, imperative logic |
| In template | {{ fullName }} | {{ getFullName() }} | Not directly |
| Performance | Excellent (lazy + cached) | Can be bad if called often | Good, but more manual |
Pro Tips from Real 2026 Projects
- Chain computeds → fine & efficient (e.g. filtered → sorted → paginated)
- Avoid side effects in computed → keep them pure (no API calls, no mutations)
- Use computed(() => …) in <script setup> (no this)
- For very expensive computations → consider shallowRef + manual trigger, but rare
- Debugging → Vue Devtools shows computed dependencies & values beautifully
Practice challenge: Extend the todo app with:
- Computed progress % (completed / total)
- Computed hasItems boolean
- Writable computed for editing todo text in place
Any part confusing? Want a full shopping cart with total + tax + discount computed? Or difference between computed vs watch in depth? Or TypeScript typing for computed?
Just tell me — we’ll keep building your Vue intuition step by step 🚀
Happy coding from Webliance, Hyderabad! 💙
