AUR0227

Error Message

AUR0227: Computed mutating

Description

This error occurs when a computed property attempts to modify (mutate) observable state during its computation. Computed properties should be pure functions that only read values and return a result without causing side effects or mutations.

Why This Matters

Computed properties are designed to be:

  • Pure: Only read values, never modify them

  • Deterministic: Always return the same result for the same inputs

  • Side-effect free: Don't trigger other state changes

When a computed property mutates state, it can cause:

  • Infinite update loops

  • Unpredictable behavior

  • Performance issues

  • Hard-to-debug race conditions

Common Scenarios

Direct Property Mutation

export class MyComponent {
  items = [1, 2, 3];
  counter = 0;
  
  // ❌ Wrong: Computed property mutating state
  get processedItems() {
    this.counter++; // This is a mutation!
    return this.items.map(x => x * 2);
  }
}

Array/Object Mutations

export class MyComponent {
  data = [{ name: 'John' }, { name: 'Jane' }];
  
  // ❌ Wrong: Mutating the original array
  get sortedData() {
    return this.data.sort((a, b) => a.name.localeCompare(b.name)); // Mutates original!
  }
  
  // ❌ Wrong: Modifying objects in computed
  get processedData() {
    return this.data.map(item => {
      item.processed = true; // Mutating original objects!
      return item;
    });
  }
}

Observable Mutations

import { observable } from '@aurelia/runtime';

export class MyComponent {
  @observable items = [];
  @observable status = 'idle';
  
  // ❌ Wrong: Changing observable state in computed
  get itemCount() {
    if (this.items.length === 0) {
      this.status = 'empty'; // This is a mutation!
    }
    return this.items.length;
  }
}

Solutions

1. Keep Computeds Pure

export class MyComponent {
  items = [1, 2, 3];
  
  // ✅ Correct: Pure computed property
  get processedItems() {
    // Only reads, doesn't mutate
    return this.items.map(x => x * 2);
  }
  
  // ✅ Correct: Count without side effects
  get itemCount() {
    return this.items.length;
  }
}

2. Use Non-Mutating Array Methods

export class MyComponent {
  data = [{ name: 'John' }, { name: 'Jane' }];
  
  // ✅ Correct: Create new sorted array
  get sortedData() {
    return [...this.data].sort((a, b) => a.name.localeCompare(b.name));
  }
  
  // ✅ Correct: Create new objects instead of mutating
  get processedData() {
    return this.data.map(item => ({
      ...item,
      processed: true
    }));
  }
}

3. Move Mutations to Methods or Effects

import { observable, computed } from '@aurelia/runtime';

export class MyComponent {
  @observable items = [];
  @observable status = 'idle';
  
  // ✅ Correct: Pure computed
  get itemCount() {
    return this.items.length;
  }
  
  // ✅ Correct: Use method for mutations
  updateStatus() {
    this.status = this.items.length === 0 ? 'empty' : 'has-items';
  }
  
  // ✅ Correct: React to changes in lifecycle or watchers
  itemsChanged() {
    this.updateStatus();
  }
}

4. Use Effects for Side Effects

import { observable, IObservation } from '@aurelia/runtime';

export class MyComponent {
  @observable items = [];
  @observable status = 'idle';
  @observable lastUpdated: Date;
  
  constructor(private observation: IObservation) {}
  
  bound() {
    // ✅ Correct: Use effect for side effects
    this.observation.run(() => {
      // This runs when items.length changes
      if (this.items.length === 0) {
        this.status = 'empty';
      } else {
        this.status = 'has-items';
      }
      this.lastUpdated = new Date();
    });
  }
  
  // ✅ Correct: Pure computed
  get itemCount() {
    return this.items.length;
  }
}

Example: Refactoring Problematic Code

// ❌ Before: Computed with mutations
export class ShoppingCart {
  @observable items = [];
  @observable total = 0;
  @observable itemCount = 0;
  
  // ❌ Wrong: Mutations in computed
  get cartSummary() {
    this.total = this.items.reduce((sum, item) => sum + item.price, 0);
    this.itemCount = this.items.length;
    return `${this.itemCount} items - $${this.total}`;
  }
}
// ✅ After: Pure computed with separate mutations
export class ShoppingCart {
  @observable items = [];
  
  // ✅ Correct: Pure computeds
  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
  
  get itemCount() {
    return this.items.length;
  }
  
  get cartSummary() {
    return `${this.itemCount} items - $${this.total}`;
  }
  
  // ✅ Correct: Mutations in methods
  addItem(item) {
    this.items.push(item);
  }
  
  removeItem(index) {
    this.items.splice(index, 1);
  }
}

Debugging Tips

  1. Review Computed Logic: Ensure computed properties only read and calculate

  2. Check for Assignments: Look for any = assignments within computed getters

  3. Verify Array Methods: Use non-mutating methods like [...array].sort() instead of array.sort()

  4. Move Side Effects: Put mutations in methods, lifecycle hooks, or effects

  5. Use DevTools: Monitor observable changes to identify unexpected mutations

  • AUR0226 - Effect maximum recursion reached

  • AUR0224 - Invalid observable decorator usage

Last updated

Was this helpful?