Watching data
Watching data for changes, including support for expressions where you want to watch for changes to one or more dependencies and react accordingly.
Introduction
The @watch
decorator lets you respond to changes in your view model properties or computed expressions. It is intended for use on custom element and attribute view models. Once a watcher is created, it binds after the binding
lifecycle and unbinds before unbinding
—meaning mutations during binding
or after unbinding
will not trigger the watcher.
Basic Usage with @watch
There are two primary ways to use @watch
:
Class-level Decoration: Attach the decorator to a class with an expression and a callback.
Method-level Decoration: Attach the decorator to a method; the method itself acts as the callback when the watched value changes.
Syntax:
import { watch } from '@aurelia/runtime-html';
// On class:
@watch(expressionOrPropertyAccessFn, changeHandlerOrCallback)
class MyClass {}
// On method:
class MyClass {
@watch(expressionOrPropertyAccessFn)
someMethod() {}
}
API Parameters
expressionOrPropertyAccessFn
string
or IPropertyAccessFn
Specifies the value to watch. When a string is provided, it is used as an expression (similar to Aurelia templating). When a function is provided, it acts as a computed getter that returns the value to observe.
changeHandlerOrCallback
string
or IWatcherCallback
Optional. The callback invoked when the watched value changes. If a string is provided, it is used to resolve a method name (resolved only once, so subsequent changes to the method are not tracked). If a function is provided, it is called with three parameters: new value, old value, and the instance.
Reacting to Property Changes
The simplest use case is to watch a single property. For example, to react whenever the name
property changes:
import { watch } from '@aurelia/runtime-html';
class NameComponent {
name = '';
@watch('name')
nameChange(newName, oldName) {
console.log('New name:', newName);
console.log('Old name:', oldName);
}
}
You can also observe expressions on arrays. For instance, watching the length of an array:
import { watch } from '@aurelia/runtime-html';
class PostOffice {
packages = [];
@watch('packages.length')
log(newCount, oldCount) {
if (newCount > oldCount) {
// New packages arrived.
} else {
// Packages were delivered.
}
}
}
Using Computed Functions
Sometimes you need to monitor changes in multiple properties. In these cases, you can provide a computed getter function to the @watch
decorator. The function should return the value you want to observe and can also register dependencies manually if needed.
Example – Watching Array Length with a Computed Getter:
import { watch } from '@aurelia/runtime-html';
class PostOffice {
packages = [];
@watch(post => post.packages.length)
log(newCount, oldCount) {
if (newCount > oldCount) {
// New packages arrived.
} else {
// Packages were delivered.
}
}
}
In this example, the callback receives the new and old computed values every time the dependency (packages.length
) changes. The view model (post
) is also passed as a parameter so you can access other properties if needed.
Usage Examples
Below are several examples illustrating different ways to use the @watch
decorator.
1. Class-level, String Expression, Arrow Function Callback
import { watch } from '@aurelia/runtime-html';
@watch('counter', (newValue, oldValue, app) => app.log(newValue))
class App {
counter = 0;
log(whatToLog) {
console.log(whatToLog);
}
}
2. Class-level, String Expression, Method Name as Callback
Warning: The method is resolved only once. Changes to the method after instance creation are not detected.
import { watch } from '@aurelia/runtime-html';
@watch('counter', 'log')
class App {
counter = 0;
log(whatToLog) {
console.log(whatToLog);
}
}
3. Class-level, String Expression, Normal Function Callback
import { watch } from '@aurelia/runtime-html';
@watch('counter', function(newValue, oldValue, app) {
// 'this' points to the instance of the class
this.log(newValue);
})
class App {
counter = 0;
log(whatToLog) {
console.log(whatToLog);
}
}
4. Class-level, Normal Function as Watch Expression, Arrow Function Callback
import { watch } from '@aurelia/runtime-html';
@watch(function(app) { return app.counter }, (newValue, oldValue, app) => app.log(newValue))
class App {
counter = 0;
log(whatToLog) {
console.log(whatToLog);
}
}
5. Class-level, Arrow Function as Watch Expression, Arrow Function Callback
import { watch } from '@aurelia/runtime-html';
@watch(app => app.counter, (newValue, oldValue, app) => app.log(newValue))
class App {
counter = 0;
log(whatToLog) {
console.log(whatToLog);
}
}
6. Method-level, String Expression
import { watch } from '@aurelia/runtime-html';
class App {
counter = 0;
@watch('counter')
log(whatToLog) {
console.log(whatToLog);
}
}
7. Method-level, Normal Function as Watch Expression
import { watch } from '@aurelia/runtime-html';
class App {
counter = 0;
@watch(function(app) { return app.counter })
log(whatToLog) {
console.log(whatToLog);
}
}
8. Method-level, Arrow Function as Watch Expression
import { watch } from '@aurelia/runtime-html';
class App {
counter = 0;
@watch(app => app.counter)
log(whatToLog) {
console.log(whatToLog);
}
}
Watch Reactivity & Lifecycle
Watchers created via the @watch
decorator activate and deactivate in sync with component lifecycles:
During
binding
: Watchers are not active. Mutations here won’t trigger callbacks.import { watch } from '@aurelia/runtime-html'; class PostOffice { packages = []; @watch(post => post.packages.length) log(newCount, oldCount) { console.log(`packages changes: ${oldCount} -> ${newCount}`); } binding() { this.packages.push({ id: 1, name: 'xmas toy', delivered: false }); } }
No log output during
binding
.During
bound
: Watchers are active. Changes will trigger the callback.import { watch } from '@aurelia/runtime-html'; class PostOffice { packages = []; @watch(post => post.packages.length) log(newCount, oldCount) { console.log(`packages changes: ${oldCount} -> ${newCount}`); } bound() { this.packages.push({ id: 1, name: 'xmas toy', delivered: false }); } }
Logs:
packages changes: 0 -> 1
.During
detaching
: Watchers are still active and will respond to changes.import { watch } from '@aurelia/runtime-html'; class PostOffice { packages = []; @watch(post => post.packages.length) log(newCount, oldCount) { console.log(`packages changes: ${oldCount} -> ${newCount}`); } detaching() { this.packages.push({ id: 1, name: 'xmas toy', delivered: false }); } }
Logs:
packages changes: 0 -> 1
.During
unbinding
: Watchers have been deactivated; changes are ignored.import { watch } from '@aurelia/runtime-html'; class PostOffice { packages = []; @watch(post => post.packages.length) log(newCount, oldCount) { console.log(`packages changes: ${oldCount} -> ${newCount}`); } unbinding() { this.packages.push({ id: 1, name: 'xmas toy', delivered: false }); } }
No log output during
unbinding
.
Info: Lifecycles between
binding
andunbinding
(such asattaching
,attached
, anddetaching
) behave normally with respect to watchers.
How It Works
When you apply @watch()
, a watcher is created to monitor the specified expression:
String or Symbol Expressions: Interpreted like Aurelia template expressions.
Function Expressions (Computed Getters): The function is called to obtain a value and register its dependencies. Two mechanisms exist:
With Native Proxy Support: Proxies intercept property reads, including collection method calls (e.g.,
.map()
), to automatically track dependencies.Without Native Proxy Support: You receive a second parameter—the watcher instance—to manually register dependencies.
The IWatcher Interface
In environments without native proxies, the computed getter receives a watcher with the following interface:
interface IWatcher {
observeProperty(obj: object, key: string | number | symbol): void;
observeCollection(collection: Array<any> | Map<any, any> | Set<any>): void;
}
Example:
import { watch } from '@aurelia/runtime-html';
class Contact {
firstName = 'Chorris';
lastName = 'Nuck';
@watch((contact, watcher) => {
// Manually observe dependencies.
watcher.observeProperty(contact, 'firstName');
watcher.observeProperty(contact, 'lastName');
return `${contact.firstName} ${contact.lastName}`;
})
validateFullName(fullName) {
if (fullName === 'Chuck Norris') {
this.faint();
}
}
}
Automatic Array Observation:
Note: In computed getters, common array mutation methods (
push
,pop
,shift
,unshift
,splice
,reverse
) are not observed automatically because they don’t expose clear dependency signals.
Best Practices
1. Avoid Mutating Dependencies in Computed Getters
Do not alter properties or collections when returning a computed value:
// Avoid:
@watch(object => object.counter++)
someMethod() {}
// Avoid these mutations:
@watch(object => object.someArray.push(...args))
@watch(object => object.someArray.pop())
@watch(object => object.someArray.shift())
@watch(object => object.someArray.unshift())
@watch(object => object.someArray.splice(...args))
@watch(object => object.someArray.reverse())
someMethod() {}
2. Be Cautious with Object Identity
Due to proxy wrapping, a raw object and its proxied version may not be strictly equal. Always access the dependency from the first parameter to maintain proper identity checks.
import { watch } from '@aurelia/runtime-html';
const defaultOptions = {};
class MyClass {
options = defaultOptions;
@watch(myClass => myClass.options === defaultOptions ? null : myClass.options)
applyCustomOptions() {
// ...
}
}
3. Do Not Return Promises or Async Functions
The dependency tracking is synchronous. Returning a promise or using an async function will break the reactivity.
import { watch } from '@aurelia/runtime-html';
class MyClass {
// Incorrect – async functions or promises are not supported
@watch(async myClassInstance => myClassInstance.options)
applyCustomOptions() {}
// Incorrect usage:
@watch(myClassInstance => {
Promise.resolve().then(() => {
return myClassInstance.options;
});
})
anotherMethod() {}
}
Last updated
Was this helpful?