Skip to main content

Vue.js Computed Properties

Introduction

When building interactive web applications with Vue.js, you'll often need to transform or derive values from your existing data. While you could perform these transformations directly in your template with inline expressions, this approach can lead to messy templates and duplicate code. This is where computed properties come to the rescue.

Computed properties are a core Vue.js feature that allows you to create derived values that automatically update when their dependencies change. Think of them as functions that cache their results until one of their reactive dependencies changes.

Why Use Computed Properties?

Before diving into the syntax and usage, let's understand why computed properties are essential:

  1. Performance optimization - Results are cached based on dependencies
  2. Clean template logic - Move complex calculations out of templates
  3. Dependency tracking - Automatically updates when dependencies change
  4. Reusability - Define once, use throughout your component

Basic Syntax

Here's the basic structure of a computed property:

html
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
}
</script>

In the template, you can use the computed property like any other data property:

html
<template>
<div>
<p>Full Name: {{ fullName }}</p>
</div>
</template>

Computed Properties vs Methods

You might wonder: "Why not just use a method?" Let's compare:

html
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName() {
console.log('Computing fullName');
return this.firstName + ' ' + this.lastName;
}
},
methods: {
getFullName() {
console.log('Method getFullName called');
return this.firstName + ' ' + this.lastName;
}
}
}
</script>

<template>
<div>
<p>Computed: {{ fullName }}</p>
<p>Computed again: {{ fullName }}</p>
<p>Method: {{ getFullName() }}</p>
<p>Method again: {{ getFullName() }}</p>
</div>
</template>

Output in console:

Computing fullName
Method getFullName called
Method getFullName called

Notice that the computed property's function runs only once, while the method runs every time it's called. This is because computed properties cache their results based on their reactive dependencies and only re-evaluate when those dependencies change.

Dependency Tracking

Computed properties automatically track their reactive dependencies. Let's see this in action:

html
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe',
age: 30
}
},
computed: {
fullName() {
console.log('Computing fullName');
return this.firstName + ' ' + this.lastName;
}
},
mounted() {
// This will trigger the fullName computed property to update
setTimeout(() => {
this.firstName = 'Jane';
}, 1000);

// This won't trigger fullName to update since age is not a dependency
setTimeout(() => {
this.age = 31;
}, 2000);
}
}
</script>

In this example, changing firstName will cause fullName to recalculate, but changing age won't, because fullName doesn't depend on it.

Computed Property Getters and Setters

By default, computed properties are get-only, but you can also provide a setter:

html
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName;
},
// setter
set(newValue) {
const names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[names.length - 1];
}
}
},
mounted() {
// After 2 seconds, set fullName which will update firstName and lastName
setTimeout(() => {
this.fullName = 'Jane Smith';
}, 2000);
}
}
</script>

<template>
<div>
<p>Full name: {{ fullName }}</p>
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
</div>
</template>

When you set this.fullName = 'Jane Smith', the setter is invoked, which splits the name and updates the original data properties.

Practical Examples

Example 1: Filtering a List

html
<script>
export default {
data() {
return {
searchQuery: '',
items: [
{ id: 1, name: 'Apple', category: 'Fruit' },
{ id: 2, name: 'Broccoli', category: 'Vegetable' },
{ id: 3, name: 'Orange', category: 'Fruit' },
{ id: 4, name: 'Carrot', category: 'Vegetable' },
{ id: 5, name: 'Banana', category: 'Fruit' }
]
}
},
computed: {
filteredItems() {
if (!this.searchQuery) {
return this.items;
}

const query = this.searchQuery.toLowerCase();
return this.items.filter(item =>
item.name.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query)
);
}
}
}
</script>

<template>
<div>
<input
v-model="searchQuery"
placeholder="Search items..."
/>

<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }} - {{ item.category }}
</li>
</ul>
</div>
</template>

In this example, filteredItems is recalculated whenever searchQuery or items changes. It uses the current search query to filter the list of items.

Example 2: Shopping Cart Total

html
<script>
export default {
data() {
return {
cart: [
{ id: 1, name: 'Product 1', price: 10, quantity: 2 },
{ id: 2, name: 'Product 2', price: 15, quantity: 1 },
{ id: 3, name: 'Product 3', price: 20, quantity: 3 }
],
taxRate: 0.08
}
},
computed: {
subtotal() {
return this.cart.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
},
tax() {
return this.subtotal * this.taxRate;
},
total() {
return this.subtotal + this.tax;
}
},
methods: {
updateQuantity(id, newQuantity) {
const item = this.cart.find(item => item.id === id);
if (item) {
item.quantity = Math.max(1, newQuantity);
}
}
}
}
</script>

<template>
<div>
<h2>Shopping Cart</h2>
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - ${{ item.price }} ×
<input
type="number"
:value="item.quantity"
@input="updateQuantity(item.id, $event.target.value)"
min="1"
style="width: 50px"
/>
= ${{ item.price * item.quantity }}
</li>
</ul>

<div>
<p>Subtotal: ${{ subtotal.toFixed(2) }}</p>
<p>Tax ({{ (taxRate * 100).toFixed() }}%): ${{ tax.toFixed(2) }}</p>
<p><strong>Total: ${{ total.toFixed(2) }}</strong></p>
</div>
</div>
</template>

This shopping cart example shows how computed properties can build upon each other: total depends on subtotal and tax, and tax depends on subtotal. When an item's quantity changes, all the computed values update automatically.

Example 3: Form Validation

html
<script>
export default {
data() {
return {
form: {
email: '',
password: '',
confirmPassword: ''
},
submitted: false
}
},
computed: {
emailError() {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!this.form.email) {
return 'Email is required';
} else if (!regex.test(this.form.email)) {
return 'Please enter a valid email address';
}
return '';
},
passwordError() {
if (!this.form.password) {
return 'Password is required';
} else if (this.form.password.length < 8) {
return 'Password must be at least 8 characters';
}
return '';
},
passwordMatchError() {
if (this.form.confirmPassword && this.form.password !== this.form.confirmPassword) {
return 'Passwords do not match';
}
return '';
},
isFormValid() {
return !this.emailError && !this.passwordError && !this.passwordMatchError;
}
},
methods: {
submitForm() {
this.submitted = true;
if (this.isFormValid) {
alert('Form is valid! Submitted.');
// Here you would typically make an API call
}
}
}
}
</script>

<template>
<form @submit.prevent="submitForm">
<div>
<label for="email">Email:</label>
<input
id="email"
v-model="form.email"
type="email"
/>
<p v-if="submitted && emailError" class="error">{{ emailError }}</p>
</div>

<div>
<label for="password">Password:</label>
<input
id="password"
v-model="form.password"
type="password"
/>
<p v-if="submitted && passwordError" class="error">{{ passwordError }}</p>
</div>

<div>
<label for="confirmPassword">Confirm Password:</label>
<input
id="confirmPassword"
v-model="form.confirmPassword"
type="password"
/>
<p v-if="submitted && passwordMatchError" class="error">{{ passwordMatchError }}</p>
</div>

<button type="submit">Register</button>
</form>
</template>

<style scoped>
.error {
color: red;
font-size: 0.8em;
}
</style>

This form validation example demonstrates how computed properties can make complex validation logic clean and maintainable.

Computed Properties in the Composition API

If you're using Vue 3's Composition API, you can use the computed function:

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

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

// Read-only computed property
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value;
});

// Computed property with getter and setter
const displayName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});

// After 2 seconds, update displayName
setTimeout(() => {
displayName.value = 'Jane Smith';
}, 2000);
</script>

<template>
<div>
<p>Full name: {{ fullName }}</p>
<p>Display name: {{ displayName }}</p>
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
</div>
</template>

Caching and Performance Considerations

Computed properties shine in performance-intensive scenarios:

html
<script>
export default {
data() {
return {
numbers: Array.from({ length: 10000 }, (_, i) => i + 1)
}
},
computed: {
evenNumbers() {
console.time('computed');
const result = this.numbers.filter(num => num % 2 === 0);
console.timeEnd('computed');
return result;
}
},
methods: {
getEvenNumbers() {
console.time('method');
const result = this.numbers.filter(num => num % 2 === 0);
console.timeEnd('method');
return result;
}
}
}
</script>

<template>
<div>
<p>First 5 even numbers (computed): {{ evenNumbers.slice(0, 5) }}</p>
<p>Count of even numbers (computed): {{ evenNumbers.length }}</p>

<p>First 5 even numbers (method): {{ getEvenNumbers().slice(0, 5) }}</p>
<p>Count of even numbers (method): {{ getEvenNumbers().length }}</p>
</div>
</template>

In the console, you'll see that the computed property runs only once, while the method runs twice. With a large array, the difference in performance can be substantial.

Common Patterns and Best Practices

  1. Keep computed properties pure

    • Avoid side effects like API calls or modifying data
    • Use watchers for side effects instead
  2. Don't access non-reactive data

    • Ensure all dependencies are properly reactive
    • Avoid properties that won't trigger updates when they should
  3. Use computed properties for derived state

    • Perfect for filtering, sorting, formatting, calculations
    • Keep template logic minimal
  4. Avoid complex logic in templates

    • Move logic to computed properties instead
    • Helps with readability and maintainability

Summary

Computed properties are a powerful feature in Vue.js that allow you to:

  • Create derived values based on reactive data
  • Optimize performance through intelligent caching
  • Keep your templates clean and focused on presentation
  • Implement reactive validations and transformations

By understanding when and how to use computed properties effectively, you'll write more maintainable, performant Vue applications that are easier to debug and extend.

Exercises

  1. Create a component with a text input and display the reversed input text using a computed property.
  2. Implement a todo list with computed properties for active, completed, and filtered todos.
  3. Build a pricing calculator with base price, quantity, discounts, and taxes using computed properties.
  4. Create a form with computed validations for email, password strength, and matching passwords.
  5. Modify the shopping cart example to include a coupon code system using computed properties.

Additional Resources

Understanding computed properties is essential for building effective Vue applications, and mastering this concept will dramatically improve your component structure and performance.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)