Reactivity

Aurelia's reactivity system automatically tracks changes to your data and updates the UI efficiently. Unlike frameworks that use virtual DOM, Aurelia observes your data directly and surgically updates only what has changed.

When to Use Which Reactivity Feature?

Aurelia offers several reactivity tools. Here's how to choose:

Use simple properties (no decorator) when:

  • You only need UI updates - Properties bound in templates are automatically observed

  • ✅ Most common case - Just declare the property and bind it

  • ✅ Example: todos: Todo[] = [] with repeat.for="todo of todos"

Use getters (computed) when:

  • Value depends on other properties and calculation is cheap

  • ✅ Automatic dependency tracking - no manual configuration needed

  • ✅ Example: get fullName() { return this.firstName + ' ' + this.lastName; }

Use @computed decorator when:

  • Expensive calculations that should be cached

  • ✅ You want to explicitly control dependencies (not automatic)

  • ✅ Deep observation needed for nested objects

  • ✅ Example: Complex filtering, heavy aggregations

Use @observable when:

  • You need to run code when a property changes (side effects)

  • ✅ You want the propertyChanged(newValue, oldValue) callback

  • ✅ Examples: Validation, analytics tracking, syncing data

Use watch() when:

  • Complex expressions - watching multiple properties or nested values

  • ✅ Need more flexibility than @observable

  • ✅ Examples: @watch('user.address.city'), @watch(vm => vm.total > 100)

Use manual observation when:

  • ✅ Building libraries or advanced features

  • ✅ Need fine-grained control over subscription lifecycle

  • ✅ Performance critical code requiring optimization

Automatic Change Detection

Aurelia automatically observes properties that are bound in your templates. No decorators or special setup required:

The ${todos.length}, value.bind="filter", and repeat.for="todo of todos" create automatic observation - Aurelia tracks changes to these properties and updates the UI accordingly.

Computed Properties

Getter properties automatically become reactive when their dependencies change:

Decorator computed

For some reason, it's more preferrable to specify dependencies of a getter manually, rather than automatically tracked on read, you can use the decorator @computed to declare the dependencies, like the following example:

You can also specify multiple properties as dependencies, like the following example:

Basides the above basic usages, the computed decorator also supports a few more options, depending on the needs of an application.

Flush timing with flush

Like how you can specify flush mode of computed getter with @computed({ flush: 'sync' }), flush mode of @computed can also be done in a similar way, like the following example:

Deep observation with deep

Sometimes you also want to automatically observe all properties of an object recursively, regardless at what level, deep option on the @computed decorator can be used to achieve this goal, like the following example:

Now whenever _cart.items[].price or _cart.items[].quantity (or whatever else properties on each element in the items array), or _cart.gst changes, the total is considered dirty.

[!WARNING] deep observation doesn't observe non-existent properties, which means newly added properties won't trigger any changes notification. Replace the entire object instead.

Deep Observation

Aurelia can observe nested object changes:

Array Observation

Arrays are automatically observed for mutations:

When You Need @observable

The @observable decorator is only needed when you want to react to property changes in your view-model code (not just the template):

Effect Observation

Create side effects that run when observed data changes:

Manual Observation Control

For advanced scenarios, manually control observation:

Performance Considerations

Aurelia's observation is highly optimized:

  • Batched Updates: Multiple changes are batched into single DOM updates

  • Surgical Updates: Only changed elements are updated, not entire component trees

  • Smart Detection: Observes only bound properties, not entire objects

  • No Virtual DOM: Direct DOM manipulation eliminates virtual DOM overhead

Common Reactivity Patterns

Pattern: Form Validation with @observable

Use case: Validate input as the user types, show errors immediately.

Why this works: @observable triggers validation automatically as users type. The isValid getter recomputes whenever errors change, enabling/disabling the submit button reactively.

Pattern: Computed Filtering and Sorting

Use case: Filter and sort a list based on user input without re-fetching data.

Why this works: The filteredProducts getter automatically recomputes when any dependency changes. No manual refresh needed - the UI stays in sync with filters.

Pattern: Syncing Data with @watch

Use case: Keep related data in sync, like saving to localStorage or syncing with a server.

Why this works: @watch observes both content and title, automatically saving changes. The pattern prevents data loss and provides user feedback.

Pattern: Dependent Computations

Use case: Chain computed properties where one depends on another.

Why this works: Computed properties automatically form a dependency chain. When subtotal changes, discount updates, which updates afterDiscount, then tax, and finally total. All cascade automatically.

Pattern: Optimized List Updates with @computed

Use case: Expensive computations on large lists that should only recalculate when necessary.

Why this works: @computed with explicit dependencies prevents unnecessary recalculations. Changing individual data point properties won't trigger recalculation - only changes to array length, date range, or metric do.

Best Practices

Choose the Right Tool

  • Start simple - Use plain properties and getters first

  • ✅ Add @observable only when you need side effects

  • ✅ Use @computed for expensive operations, not simple getters

  • ❌ Don't over-engineer - most scenarios don't need @watch or manual observation

Keep Computations Pure

  • ✅ Computed getters should have no side effects

  • ✅ Same inputs should always produce same outputs

  • ❌ Don't modify state inside getters

  • ❌ Don't make API calls in computed properties

Optimize Performance

  • ✅ Use @computed with explicit dependencies for expensive calculations

  • ✅ Debounce rapid changes (user input, scroll events)

  • ✅ Batch related updates together

  • ❌ Don't create unnecessary watchers

Handle Async Operations

  • ✅ Use @watch or propertyChanged callbacks for async side effects

  • ✅ Track loading states during async operations

  • ✅ Handle errors gracefully

  • ❌ Don't make computed properties async

What's Next

Last updated

Was this helpful?