Computed Properties for Derived State

+15 Mana ✨

Computed properties let you declare derived state—values that depend on other reactive values and update automatically when their dependencies change.

Why Computed Properties?

You could put complex logic directly in templates:

vue
<template>
  <!-- This works but is messy and repetitive -->
  <p>{{ items.filter(item => item.done).length }} completed</p>
  <p>{{ items.filter(item => item.done).length }} of {{ items.length }}</p>
</template>

Computed properties solve this by:

  • Moving logic out of templates
  • Caching results for performance
  • Automatically updating when dependencies change

Basic Usage

vue
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// Computed property - automatically updates when firstName or lastName changes
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})
</script>

<template>
  <p>First: {{ firstName }}</p>
  <p>Last: {{ lastName }}</p>
  <p>Full: {{ fullName }}</p>
  
  <input v-model="firstName" placeholder="First name" />
  <input v-model="lastName" placeholder="Last name" />
</template>

When you type in either input, fullName automatically updates!

Computed vs Methods

Method Approach

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

// Method - called every time the template renders
function doubleCount() {
  console.log('Method called')  // Logs on EVERY render
  return count.value * 2
}
</script>

<template>
  <p>{{ doubleCount() }}</p>
  <p>{{ doubleCount() }}</p>
  <p>{{ doubleCount() }}</p>
  <!-- Method runs 3 times per render! -->
</template>

Computed Approach

vue
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)

// Computed - cached based on dependencies
const doubleCount = computed(() => {
  console.log('Computed evaluated')  // Logs only when count changes
  return count.value * 2
})
</script>

<template>
  <p>{{ doubleCount }}</p>
  <p>{{ doubleCount }}</p>
  <p>{{ doubleCount }}</p>
  <!-- Same cached value used 3 times -->
</template>

Key difference: Computed properties are cached and only re-evaluate when their reactive dependencies change.

Practical Examples

Filtering a List

vue
<script setup>
import { ref, computed } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', done: true },
  { id: 2, text: 'Build an app', done: false },
  { id: 3, text: 'Deploy', done: false }
])

const filter = ref('all')  // 'all', 'active', 'completed'

const filteredTodos = computed(() => {
  switch (filter.value) {
    case 'active':
      return todos.value.filter(t => !t.done)
    case 'completed':
      return todos.value.filter(t => t.done)
    default:
      return todos.value
  }
})

const stats = computed(() => ({
  total: todos.value.length,
  completed: todos.value.filter(t => t.done).length,
  remaining: todos.value.filter(t => !t.done).length
}))
</script>

<template>
  <div>
    <button @click="filter = 'all'">All ({{ stats.total }})</button>
    <button @click="filter = 'active'">Active ({{ stats.remaining }})</button>
    <button @click="filter = 'completed'">Completed ({{ stats.completed }})</button>
  </div>
  
  <ul>
    <li v-for="todo in filteredTodos" :key="todo.id">
      {{ todo.text }}
    </li>
  </ul>
</template>

Sorting Data

vue
<script setup>
import { ref, computed } from 'vue'

const products = ref([
  { id: 1, name: 'Laptop', price: 999 },
  { id: 2, name: 'Phone', price: 699 },
  { id: 3, name: 'Tablet', price: 499 }
])

const sortBy = ref('name')  // 'name', 'price'
const sortOrder = ref('asc')  // 'asc', 'desc'

const sortedProducts = computed(() => {
  const sorted = [...products.value].sort((a, b) => {
    const aVal = a[sortBy.value]
    const bVal = b[sortBy.value]
    
    if (typeof aVal === 'string') {
      return aVal.localeCompare(bVal)
    }
    return aVal - bVal
  })
  
  return sortOrder.value === 'desc' ? sorted.reverse() : sorted
})
</script>

Form Validation

vue
<script setup>
import { ref, computed } from 'vue'

const email = ref('')
const password = ref('')
const confirmPassword = ref('')

const errors = computed(() => {
  const errs = []
  
  if (!email.value) {
    errs.push('Email is required')
  } else if (!/^[^@]+@[^@]+\.[^@]+$/.test(email.value)) {
    errs.push('Invalid email format')
  }
  
  if (password.value.length < 8) {
    errs.push('Password must be at least 8 characters')
  }
  
  if (password.value !== confirmPassword.value) {
    errs.push('Passwords do not match')
  }
  
  return errs
})

const isValid = computed(() => errors.value.length === 0)
</script>

<template>
  <form @submit.prevent>
    <input v-model="email" type="email" placeholder="Email" />
    <input v-model="password" type="password" placeholder="Password" />
    <input v-model="confirmPassword" type="password" placeholder="Confirm" />
    
    <ul v-if="errors.length" class="errors">
      <li v-for="error in errors" :key="error">{{ error }}</li>
    </ul>
    
    <button :disabled="!isValid">Submit</button>
  </form>
</template>

Writable Computed Properties

Computed properties are read-only by default. For writable computed, provide getter and setter:

vue
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const parts = newValue.split(' ')
    firstName.value = parts[0] || ''
    lastName.value = parts.slice(1).join(' ') || ''
  }
})
</script>

<template>
  <!-- Two-way binding works! -->
  <input v-model="fullName" />
  <p>First: {{ firstName }}</p>
  <p>Last: {{ lastName }}</p>
</template>
✓ Completed