Never Mutate Computed Property Return Values
Impact: HIGH - The returned value from a computed property is derived state - a temporary snapshot. Mutating this value leads to bugs that are difficult to debug.
Important: Mutations DO persist while the computed cache remains valid, but are lost when recomputation occurs. The danger lies in unpredictable cache invalidation timing - any change to the computed's dependencies triggers recomputation, silently discarding your mutations. This makes bugs intermittent and hard to reproduce.
Every time the source state changes, a new snapshot is created. Mutating a snapshot is meaningless because it will be discarded on the next recalculation.
Task Checklist
- [ ] Treat computed return values as read-only
- [ ] Update the source state instead of the computed value
- [ ] Use writable computed properties if bidirectional binding is needed
- [ ] Avoid array mutating methods (push, pop, splice, reverse, sort) on computed arrays
Incorrect:
<script setup>
import { ref, computed } from 'vue'
const books = ref(['Vue Guide', 'React Handbook'])
const publishedBooks = computed(() => {
return books.value.filter(book => book.includes('Guide'))
})
function addBook() {
// BAD: Mutating computed value - change will be lost!
publishedBooks.value.push('New Book')
}
// BAD: Mutating computed array
const sortedBooks = computed(() => books.value.filter(b => b))
function reverseBooks() {
// BAD: This mutates the computed snapshot
sortedBooks.value.reverse()
}
</script><script>
export default {
data() {
return {
author: {
name: 'John',
books: ['Book A', 'Book B']
}
}
},
computed: {
authorBooks() {
return this.author.books
}
},
methods: {
addBook() {
// BAD: Mutating computed value
this.authorBooks.push('New Book')
}
}
}
</script>Correct:
<script setup>
import { ref, computed } from 'vue'
const books = ref(['Vue Guide', 'React Handbook'])
const publishedBooks = computed(() => {
return books.value.filter(book => book.includes('Guide'))
})
function addBook(bookName) {
// GOOD: Update the source state
books.value.push(bookName)
}
// GOOD: Create a copy before mutating for display
const sortedBooks = computed(() => {
return [...books.value].sort() // Spread to create copy before sort
})
const reversedBooks = computed(() => {
return [...books.value].reverse() // Spread to create copy before reverse
})
</script><script>
export default {
data() {
return {
author: {
name: 'John',
books: ['Book A', 'Book B']
}
}
},
computed: {
authorBooks() {
return this.author.books
}
},
methods: {
addBook(bookName) {
// GOOD: Update source state
this.author.books.push(bookName)
}
}
}
</script>Writable Computed for Bidirectional Binding
If you genuinely need to "set" a computed value, use a writable computed property:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// Writable computed with getter and setter
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
// Update source state based on the new value
const parts = newValue.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
}
})
// Now this is valid:
fullName.value = 'Jane Smith' // Updates firstName and lastName
</script>