ArcGIS Blog

Developers

ArcGIS Maps SDK for JavaScript

Why you should be using reactiveUtils instead of watchUtils

By Hugo Campos and Andy Gup and Lauren Boyd

Observing for changes is one of the most important aspects of managing application life-cycle. At version 4.23 (March 2022) we introduced a new module to the ArcGIS Maps SDK for JavaScript (JavaScript Maps SDK) called reactiveUtils to provide developers with significantly improved watch tooling. This module contains utilities for observing changes in the state of properties using reactivity, which is based on the concepts of reactive programming. It is meant to replace the deprecated watchUtils module, and its utilities have a wider range of capabilities, such as the ability to watch almost anything, being TypeScript safe and more scalable. In this blog post, we will highlight some of the differences between the two modules and explain why you should migrate your app to these new utilities.

 

From watchUtils to reactiveUtils

When we originally introduced Accessor and reactive properties, we didn’t have support for reactivity. Accessor is the parent class for many of the SDKs classes. Developers had to manually specify dependencies between properties. For example, you could use the watchUtils module as follows using a string to set the property name:

// Call the callback when view.stationary changes.
watchUtils.watch(view, 'stationary', (stationary) => {
  // Do something with the new value
});
// Call the callback when view.interacting or view.navigating change.
watchUtils.watch(view, ['interacting', 'navigating'], () => {
  // Do something
});
// Call the callback when the view becomes stationary.
watchUtils.when(view, 'stationary', () => {
  // Do something when the view becomes stationary.
});
// Call the callback when the state is equal to “ready”
watchUtils.whenEqualOnce(widget, 'state', 'ready', () => {
  // Do something when the state is “ready”.
});

The watchUtils pattern is always similar: provide the object to be tracked, one or several of its properties as strings, and a callback which is called when a property changes or when a condition is met.

Let’s see how to implement the same examples with the new reactiveUtils:

// Call the callback when a property changes.
reactiveUtils.watch(
  () => view.stationary, 
  (stationary) => {
  // Do something with the new value
});
// Call the callback when view.interacting or view.navigating change.
reactiveUtils.watch(
  () => [view.interacting, view.navigating], 
  ([interacting, navigating]) => {
  // Do something
});
// Call the callback when the view becomes stationary.
reactiveUtils.when(
  () => view.stationary, 
  () => {
    // Do something when the view becomes stationary.
});
// Call the callback when the state is equal to “ready”
reactiveUtils.when(
  () => widget.state === 'ready', 
  () => {
    // Do something when the state is “ready” one time.
}, 
{ once: true });

As you can see, the pattern is slightly different, you don’t need to specify which properties are to be observed. Instead, we provide a reactive expression as the first argument to the watch() and when() functions. An expression is code that resolves to a value. This expression is evaluated and, when its result changes (in the case of watch) or becomes truthy (in the case of when), the callback is called.

One way to think about it is to compare it to Excel spreadsheet. In an Excel spreadsheet, you can create formulas that reference other cells (properties) and compute a result. The formula is like a reactive expression, and the cell containing the formula (where the result is displayed) is like a reactiveUtils callback, which receives the result of evaluating the reactive expression.

// Evaluate the reactive expression and, when it changes, call the callback with the result.
reactiveUtils.watch(
  reactiveExpression, 
  (result) => {
    // Do something
  }, options);

As we can see in the previous examples, we can also provide some options to control how a watch behaves, such as the ReactiveWatchOptions properties once, sync, initial and equals:

// Run synchronously, in the same tick, as soon as a property changes.
reactiveUtils.watch(
  () => {}, 
  callback, 
  { sync: true });

// Run right away with the initial result of the expression.
reactiveUtils.watch(
  () => {}, 
  callback, 
  { initial: true });

// Provide a custom equality function. The callback will only be called if
// two consecutive results are not the same.
reactiveUtils.watch(
  () => {}, 
  callback, 
  { equals: (a, b) => a === b});

Other features and benefits

So far, we’ve only seen how we can replicate things which was also possible with watchUtils. However, reactiveUtils are significantly more advanced. We’ll now look at some of the benefits of using the reactiveUtils.

You can observe (almost) anything

Since we can pass any expression to the reactiveUtils, we can observe almost anything. For example, let’s observe changes in a Collection.

reactiveUtils.watch(
  () => view.map.layers.filter((layer) => layer.visible).toArray(),
  (visibleLayers) => {
    // Do something with the visibleLayers Collection
  }
);

With this watch, the callback will be called anytime the list of visible layers changes. If a layer is added/removed/moved, the callback will be called with an updated list. And if any of the layers in the Collection has their visible property changed, the callback will also be called with an updated list of layers.

Note, currently reactiveUtils doesn’t support observing native arrays, Map or Set.

Automatic dependencies

One of the key benefits of using reactiveUtils is that it automatically tracks and manages dependencies between different Accessor properties and your application. With reactiveUtils, you never reference properties by name like you do with watchUtils. Instead, any property accessed in a reactive expression is automatically tracked and depended on by the watch. This means that you don’t have to worry about forgetting to include a dependency in your watch, because reactiveUtils will automatically track all dependencies for you.

Moreover, reactiveUtils automatically manages dependencies in a dynamic and efficient way. If you modify a reactive expression and it no longer depends on a certain property, that property will no longer be observed, which can save computation time and improve performance. For example, consider the following code:

reactiveUtils.watch(
  () => view.stationary ? view.map.layers.toArray() : [],
  (layers) => {
    // Do something with the layers
  }
);

In this case, the reactive expression contains a conditional operator that depends on the view.stationary Boolean property. This means that the watch will always depend on view.stationary, because the conditional is always accessed in the reactive expression. However, the watch will only observe the view.map.layers Collection while view.stationary evaluates as true.

Much better refactoring and TypeScript support

Having properties accessed directly in an expression also means that code refactoring tools, such as Typescript, will work out-of-the-box. If you use Typescript to refactor an Accessor property in your code, the change will be automatically propagated throughout your codebase and all reactive watches will continue to work correctly.

In contrast, when using watchUtils, you need to manually update every single watch that specifies a dependency on a property that you are refactoring. This can be error-prone and time-consuming, and it’s easy to forget to update some watches.

Furthermore, reactiveUtils allows Typescript to automatically infer the types of properties in your reactive expression. This means that you don’t have to manually specify the types of the callback arguments, which can save time, reduce or eliminate errors as well as make your code easier to read. For example, consider the following code:

// Typescript doesn’t know that fullName is a string, so we need to specify it.
// If you modify the type in the User class, you need to manually update it here.
watchUtils.watch(user, 'fullName' ,(fullName: string) => {
  // Do something
});

// Typescript automatically infers the type of the return value of the reactive expression.
reactiveUtils.watch(
  () => user.fullName,
  (fullName) => { // Type inferred as string!
    // Do something
  }
);

In the first example, we are using watchUtils and we need to manually specify the type of the callback argument. If we modify the type of the fullName property in the User class, we also need to manually update the type in the watch callback. In contrast, the second example is using reactiveUtils and Typescript automatically infers the type of the fullName property based on the return value of the reactive expression. This means that you don’t have to manually specify the type, and if you modify the type in the User class, it will be automatically updated in the watch.

A note about refactoring from watchUtils to reactiveUtils

In the reactiveUtils API reference page, there are hints as to which reactiveUtils method is recommended as a replacement for each watchUtils method. There is also additional information in the Overview section of the reactiveUtils API reference.

Conclusion

As we’ve seen, the reactiveUtils provide valuable tools and greater flexibility for responding to changes in API objects and your own Accessor-based objects. They offer enhanced safety and bug prevention, and when used with Typescript, facilitate efficient refactoring. Compared to the now removed watchUtils, the reactiveUtils offer a superior solution for managing property and state changes in a wide range of contexts.

If you want to continue learning about reactivity, here are some recommended readings:

Share this article

Subscribe
Notify of
0 Comments
Oldest
Newest
Inline Feedbacks
View all comments