Introduction
Vue computed properties cache their results and only re-evaluate when their reactive dependencies change. When a prop is mutated in a way that Vue's reactivity system does not track, the computed property returns a stale cached value:
computed: {
totalPrice() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
}
// Returns stale total when item.quantity is changed directlySymptoms
- Computed property returns old values after data changes
- Template displays outdated computed values
- Vue DevTools shows the computed property has not re-evaluated
- Direct mutation of object properties inside arrays does not trigger recomputation
- Computed property works after a manual page refresh but not after in-place data changes
Common Causes
- Mutating nested properties that Vue 2 cannot detect (same as array index issue)
- Replacing a prop reference without using reactive assignment
- Computed property depends on a non-reactive variable (e.g., a plain variable, not
data) - Prop mutated by a parent component but child's computed property does not re-evaluate due to reference equality
- Using
Object.assignor spread operator in a way that breaks reactivity tracking
Step-by-Step Fix
- 1.Ensure all nested property changes are reactive. In Vue 2, use
$set: - 2.```javascript
- 3.// In the parent component
- 4.updateQuantity(itemIndex, newQuantity) {
- 5.// BAD: Vue 2 won't detect this
- 6.this.items[itemIndex].quantity = newQuantity;
// GOOD: use $set this.$set(this.items[itemIndex], 'quantity', newQuantity); } ```
- 1.Use a deep watcher when the computed property depends on complex nested objects:
- 2.```javascript
- 3.export default {
- 4.props: ['items'],
- 5.computed: {
- 6.totalPrice() {
- 7.return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
- 8.}
- 9.},
- 10.watch: {
- 11.items: {
- 12.handler() {
- 13.// Force recomputation by accessing the computed property
- 14.// This is a workaround for Vue 2's shallow dependency tracking
- 15.this.$forceUpdate();
- 16.},
- 17.deep: true
- 18.}
- 19.}
- 20.}
- 21.
` - 22.Return new objects from computed properties to ensure downstream reactivity:
- 23.```javascript
- 24.computed: {
- 25.processedItems() {
- 26.// Return a new array, not a reference
- 27.return this.items.map(item => ({
- 28....item,
- 29.total: item.price * item.quantity
- 30.}));
- 31.}
- 32.}
- 33.
` - 34.In Vue 3, use
reactivewith proper structure: - 35.```javascript
- 36.import { reactive, computed } from 'vue';
const state = reactive({ items: [ { name: 'Item 1', price: 10, quantity: 2 }, ] });
const totalPrice = computed(() => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0) );
// This works in Vue 3 - Proxy detects nested mutations state.items[0].quantity = 5; console.log(totalPrice.value); // Correctly returns 50 ```
Prevention
- In Vue 2, always use
$setfor nested property mutations - Prefer computed properties that operate on flat reactive data structures
- In Vue 3, leverage Proxy-based reactivity but still prefer immutable patterns for predictability
- Write unit tests that mutate props and verify computed properties update
- Use Vue DevTools to inspect computed property dependencies and cache status
- Avoid mutating props directly - use emit events to request parent changes
- For complex derived state, consider using Pinia store with explicit reactive getters