Skip to content

Data sources: Watchers

Watchers are similar to computed properties: They are JavaScript functions, and they trigger automatically when the data they reference changes. But while computed properties should only return values and never include side-effects, watchers are the opposite: They exist to do stuff when data changes, and do not return values.

You can add them to any data source via the dropdown menu:

Adding a watcher via the dropdown menu

Example 1: Log data changes

For example, let's suppose you have a scoreboard widget with a data source for receving its data. If you wanted to log the score any time it changes for debugging purposes, you could add a watcher like this:

js
const { home, away } = context.data;

console.log('Score changed:', home, '-', away);

This is similar to adding triggers to the home and away data properties, to call a function when their values change. But watchers have some useful benefits:

  1. If home and away both change at the same time, this watcher will still only fire once.
  2. Watchers also work for properties added dynamically with context.reactiveSet(), while triggers can only be added to data properties that have been defined via the Studio UI (more on reactiveSet under Functions.

Example 2: Sorting an array in place

Another scenario where watchers are useful, is to sort/filter large data sets without duplicating them. For example, imagine you have a weather API that returns a list of cities. So sort them alphabetically, it is common to use a computed property that returns a sorted duplicate:

javascript
/* Option 1: Computed property */

const { cities } = context.data.Response;
const { order } = context.sources.State;

// 1. Create a new array, so we don't mutate the original
// as an unintended side effect
const shallowCopy = [...cities];

// 2. Sort our duplicate
shallowCopy.sort((a, b) => {
  if (order === "ascending") {
    return a.name < b.name ? -1 : 1;
  } else {
    return a.name > b.name ? -1 : 1;
  }
});

// 3. Return it
return shallowCopy;

This is a clean and robust solution for most use cases. However, if your application is data heavy, you may wish to accomplish this without having two copies of the same data in memory. This is where a watcher can be useful, intentionally sorting the array in place when it (or State.order) changes:

javascript
/* Option 2: Watcher */

const { cities } = context.data.Response;
const { order } = context.sources.State;

// Simply sort the original. A watcher is not triggered
// again by its own mutations, so this is fine:
cities.sort((a, b) => {
  if (order === "ascending") {
    return a.name < b.name ? -1 : 1;
  } else {
    return a.name > b.name ? -1 : 1;
  }
});

Avoiding infinite loops

Remember that watchers are reactive – they trigger automatically any time any relevant data changes. You should therefore be careful when reading or writing to the same properties from multiple watchers. Circular references between watchers can cause an infinite loop, where they trigger each other over and over again until your app crashes.

Consider the following two watchers for example:

javascript
/* Watcher A */
State.fullName = `${State.firstName} ${State.lastName}`

/* Watcher B */
if (State.fullName.length > 24) {
  State.firstName = State.firstName[0];
}

In this admittedly contrived example, Watcher B changes the first name to just the initial, if the full name combined is longer than 24 characters. But Watcher A depends on firstName, triggering it to run again. Which sets fullName, triggering Watcher B and so on and so on...

In this case, it would be better to either combine them into a single watcher, or use a computed property that returns the shortened name without mutating the original data.