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:

javascript
computed: {
    totalPrice() {
        return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    }
}
// Returns stale total when item.quantity is changed directly

Symptoms

  • 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.assign or spread operator in a way that breaks reactivity tracking

Step-by-Step Fix

  1. 1.Ensure all nested property changes are reactive. In Vue 2, use $set:
  2. 2.```javascript
  3. 3.// In the parent component
  4. 4.updateQuantity(itemIndex, newQuantity) {
  5. 5.// BAD: Vue 2 won't detect this
  6. 6.this.items[itemIndex].quantity = newQuantity;

// GOOD: use $set this.$set(this.items[itemIndex], 'quantity', newQuantity); } ```

  1. 1.Use a deep watcher when the computed property depends on complex nested objects:
  2. 2.```javascript
  3. 3.export default {
  4. 4.props: ['items'],
  5. 5.computed: {
  6. 6.totalPrice() {
  7. 7.return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  8. 8.}
  9. 9.},
  10. 10.watch: {
  11. 11.items: {
  12. 12.handler() {
  13. 13.// Force recomputation by accessing the computed property
  14. 14.// This is a workaround for Vue 2's shallow dependency tracking
  15. 15.this.$forceUpdate();
  16. 16.},
  17. 17.deep: true
  18. 18.}
  19. 19.}
  20. 20.}
  21. 21.`
  22. 22.Return new objects from computed properties to ensure downstream reactivity:
  23. 23.```javascript
  24. 24.computed: {
  25. 25.processedItems() {
  26. 26.// Return a new array, not a reference
  27. 27.return this.items.map(item => ({
  28. 28....item,
  29. 29.total: item.price * item.quantity
  30. 30.}));
  31. 31.}
  32. 32.}
  33. 33.`
  34. 34.In Vue 3, use reactive with proper structure:
  35. 35.```javascript
  36. 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 $set for 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