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
Unlike other observation strategies detailed in the observation section, the @watch
functionality allows you to watch for changes in your view models. If you worked with the computedFrom
decorator in Aurelia 1, the watch decorator is what you would replace it with.
Watching values with @watch
Aurelia provides a way to author your components in a reactive programming model with the @watch
decorator. Decorating a class or a method inside it with the @watch
decorator, the corresponding method will be called whenever the watched value changes.
Intended usages
The
@watch
decorator can only be used on custom element and attribute view models.Corresponding watchers of
@watch
decorator will be created once, bound afterbinding
, and unbound beforeunbinding
lifecycles. This means mutation duringbinding
/ afterunbinding
won't be reacted to, as watchers haven't started or have stopped.
APIs
expressionOrPropertyAccessFn
string | IPropertyAccessFn
Watch expression specifier
changeHandlerOrCallback
string | IWatcherCallback
The callback that will be invoked when the value evaluated from watch expression has changed. If a name is given, it will be used to resolve the callback ONCE
. This callback will be called with 3 parameters: (1st) new value from the watched expression. (2nd) old value from the watched expression (3rd) the watched instance. And the context of the function call will be the instance, same with the 3rd parameter.
Reacting to property changes with @watch
The @watch
decorator allows you to react to changes on a property or computed function.
In the following example, we will call a function every time the name property in our view model is changed.
This is referred to in Aurelia as an expression. You can also observe properties on things like arrays for changes which might be familiar to you if you worked with Aurelia 1 and the @computedFrom
decorator.
This is what an expression looks like observing an array length for changes:
Using computed functions to react to changes
Sometimes you want to watch multiple values in a component, so you need to create an expression. A computed function is a function provided to the @watch
decorator, which allows you to do comparisons on multiple values.
To illustrate how you can do this, here is an example:
In this example, the log
method of PostOffice
will be called whenever a new package is added to, or an existing package is removed from the packages
array. The first argument of our callback function is the view model, allowing us to access class properties and methods.
Usage examples
Decorating on a class, string as watch expression, with arrow function as a callback
Decorating on a class, string as watch expression, with method name as a callback
The method name will be used to resolve the function once, which means changing the method after the instance has been created will not be recognized.
Decorating on a class, string as watch expression, with normal function as a callback
Decorating on a class, normal function as watch expression, with arrow function as callback
Decorating on a class, arrow function as watch expression, with arrow function as callback
Decorating on a method, string as watch expression
Decorating on a method, normal function as watch expression
Decorating on a method, arrow function as watch expression
@watch reactivity examples
During
binding
lifecycle, bindings created by@watch
decorator haven't been activated yet, which means mutations won't be reacted to:
There will be no log in the console.
During
bound
lifecycle, bindings created by@watch
decorator have been activated, and mutations will be reacted to:
There will be 1 log in the console that looks like this: packages changes: 0 -> 1
.
Other lifecycles that are invoked after binding
and before unbinding
are not sensitive to the working of the @watch
decorator, and thus don't need special mentions. Those lifecycles are attaching
, attached
, and detaching
.
During
detaching
lifecycle, bindings created by@watch
decorator have not been de-activated yet, and mutations will still be reacted to:
There will be 1 log in the console that looks like this: packages changes: 0 -> 1
.
During
unbinding
lifecycle, bindings created by@watch
decorator have been deactivated, and mutations won't be reacted to:
There will be no log in the console.
How it works
By default, a watcher will be created for a @watch()
decorator. This watcher will start observing before bound
lifecycle of components. How the observation works will depend on the first parameter given.
If a string or a symbol is given, it will be used as an expression to observe, similar to how an expression in Aurelia templating works.
If a function is given, it will be used as a computed getter to observe dependencies and evaluate the value to pass into the specified method. Two mechanisms can be employed:
For JavaScript environments with native proxy support: Proxy will be used to trap & observe property read. It will also observe collections (such as an array, map and set) based on invoked methods. For example, calling
.map(item => item.value)
on an array should observe the mutation of that array and the propertyvalue
of each item inside the array.For environments without native proxy support: the 2nd parameter inside the computed getter can be used to observe (or register) dependencies manually. This is the corresponding watcher created from a
@watch
decorator. It has the following interface:An example is:
The
firstName
andlastName
properties ofcontact
components are being observed manually. And every time eitherfirstName
, orlastName
change, the computed getter is run again, and the dependencies will be observed again. Observers are cached, and the same observer won't be added more than once, old observers from the old computed getter run will also be disposed of, so you won't have to worry about stale dependencies or memory leaks.
Automatic array observation
By default, in the computed getter, array mutation method such as
.push()
,.pop()
,.shift()
,.unshift()
,.splice()
, and.reverse()
are not observed, as there are no clear indicators of the dependencies to collect from those methods.
Best practices
Avoid mutations on dependencies inside a computed getter
It is best to avoid mutation on dependencies collected inside a computed getter.
Be careful with objects not access from the first parameter
To ensure identity equality with proxies, always be careful with objects not accessed from the first parameter passed into the computed getter. Better get the raw underlying object before doing the strict comparison with ===
.
In this example, even if options
on a MyClass
instance has never been changed, the comparison of myClass.options === defaultOptions
will still return false, as the actual value for myClass.options
is a proxied object wrapping the real object, and thus is always different with defaultOptions
.
Don't return promises or async functions
Dependency tracking inside a watch computed getter is done synchronously, which means returning a promise or having an async function won't work properly.
Don't do the following:
Last updated