import { watch, when, on, once, whenOnce } from "@arcgis/core/core/reactiveUtils.js";const { watch, when, on, once, whenOnce } = await $arcgis.import("@arcgis/core/core/reactiveUtils.js");- Since
- ArcGIS Maps SDK for JavaScript 4.23
- Overview
- Using reactiveUtils
- Truthy checks versus explicit value checks
- Async versus sync callbacks
- Working with collections
- Working with objects
- ResourceHandles and Promises
- Working with TypeScript
Overview
reactiveUtils provides a declarative way to watch SDK state and respond to changes.
To use it, provide:
- A reactive expression that returns the state you want to track.
- A callback that runs whenever that state changes.
The reactive expression is a function that automatically re-evaluates whenever any state it accesses changes.
For example, if the expression reads the map component's stationary property,
the callback runs whenever stationary changes between true and false.
Reactive expressions can track primitive values, arrays, collections, and objects.
Depending on the function you use, reactiveUtils supports both continuous observation and one-time conditions.
Using reactiveUtils
reactiveUtils provides five functions for observing state:
on(), once(), watch(), when() and whenOnce().
Each of these has different characteristics and use cases, as summarized in the table below:
| Function | Runs multiple times | Resolves once | Returns | Common use |
|---|---|---|---|---|
| watch() | Yes | No | ResourceHandle | Continuously observe for changes |
| when() | Yes | No | ResourceHandle | Run when expression becomes truthy |
| on() | Yes | No | ResourceHandle | Observe events |
| once() | No | Yes | Promise | Wait for first emitted value |
| whenOnce() | No | Yes | Promise | Wait until expression becomes truthy |
Read More
The snippet below uses watch() to react when the Map component's stationary property changes. This pattern is useful when work should run after navigation settles, such as querying features for the current extent, refreshing summary statistics, or enabling UI actions.
The first argument in the watch() function is the reactive expression, in the form of a getValue function, that evaluates the
stationary property. When a change is observed in the expression the new value is automatically passed to the callback:
// Watching for changes on the map component's stationary propertyconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.watch( // getValue expression () => viewElement.stationary, // callback (stationary) => { console.log(stationary) });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();Other examples of getValue expressions include:
() => viewElement.ready // Wait until the view finishes updating() => [viewElement.stationary, viewElement.zoom] // Track changes to multiple properties at once() => viewElement.popupElement?.open // Detect when a popup is opened() => layer.visible // Track changes in layer visibilityBasic usage rules:
- Shape expressions so callbacks receive defined values.
- Avoid truthy checks when falsy values like
0,"", orfalseare valid. - Use explicit comparisons for specific values (e.g.,
=== 0or=== false). - Use optional chaining (
?.) to safely access nested properties. - Return derived values for collections to react to structural changes.
Truthy checks versus explicit value checks
The when() and whenOnce() functions execute when the getValue expression
evaluates as truthy, instead of reacting to every value change.
Use truthy checks when you only care whether a value is present or available. Use explicit value checks when you need to verify a specific state or distinguish between different values.
| Goal | Recommended approach |
|---|---|
| Wait for property existence | Truthy |
| Wait for async resource | Truthy |
| Check boolean state (true or false) | Explicit |
Allow values such 0 or "" | Explicit |
Distinguish null or undefined | Explicit |
| Implement exact application logic | Explicit |
Read More
when() and whenOnce() functions only trigger the callback when the expression changes and then the value satisfies the expression,
such as false -> true -> false, but not true -> true.
Be careful with initial and default property values. For example, if you are watching for a property that is true by default,
such as FeatureLayer.visible, using a truthy check will not trigger the callback
until the property becomes false and then true again, which may not be the intended behavior.
The snippets below use the Popup component's open property in the getValue expression.
The first snippet uses a truthy check to determine when the popup is open,
which is useful when you want to run the callback whenever the popup becomes visible.
The property is false by default, so the callback will run when the popup opens,
but not on subsequent changes that keep it open. Once the popup is closed, the callback will run again when it opens.
const viewElement = document.querySelector("arcgis-map");
const handle = reactiveUtils.when(() => viewElement.popupElement.open, () => { console.log("Truthy check for popup open");});
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();Other truthy examples include:
() => viewElement.ready() => layer.loaded() => layer.visibleUse explicit value checks when your logic depends on a specific value, rather than simply whether a value is truthy.
This is useful when valid values may be falsy, such as 0, "", or
false, and a truthy check would produce unintended results.
For example, to react only when the map is at a specific scale:
const handle = reactiveUtils.when(() => viewElement.scale === 5000, () => { console.log("The map is at scale 1:5000");});
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();Other examples include:
() => viewElement.updating === false() => layer.visible === falseSee the MDN documentation to learn more about truthy and falsy values.
Async versus sync callbacks
Understanding the difference between asynchronous and synchronous callbacks is essential when working with reactiveUtils.
The timing of when your callback executes, either immediately or on the next microtask,
can impact both application behavior and performance.
Read More
By default, watch(), when() and on()
functions are asynchronous, meaning that the callback will run on the next microtask after the
expression value changes. This allows for batching of multiple changes and can help with
performance. However, if you need the callback to run synchronously with the change, you
can set the sync option to true in the options parameter.
once() and whenOnce() resolve via a Promise,
so they are always asynchronous and cannot be made synchronous. If the condition is
already satisfied when once() or whenOnce() is called, the promise will resolve
on the next microtask with the current value.
One nuance, if you provide initial: true in the ReactiveWatchOptions
for watch() and when(), the callback will run immediately after initialization if the
condition is met, and then continue to watch for changes. In this case, the callback runs
synchronously with the initial check, but subsequent calls are asynchronous.
This is useful when you don’t want to duplicate initialization logic or manually “seed” state.
const handle = reactiveUtils.when( () => layer.visible === true, () => showLayerUI(), { initial: true });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();Without initial: true, you might miss the fact that layer.visible was already true at startup and only respond to future changes.
Working with collections
reactiveUtils can observe changes with a Collection, such as the Map component’s
allLayerViews.
Methods like Collection.map() and Collection.filter(),
can be used in the getValue expression to derive values.
This ensures the callback reacts to additions, removals, or updates in the collection.
const handle = reactiveUtils.watch( () => viewElement.allLayerViews.map((layerView) => layerView.visible), (layerViews) => { console.log(`Visible layerViews: ${layerViews}`); });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();
Working with objects
reactiveUtils allows you to track object properties using dot notation
(e.g. viewElement.updating) or bracket notation (e.g. viewElement["updating"]).
Optional chaining (?.) can be used to safely access nested properties
without manually checking each level.
const handle = reactiveUtils.watch( () => viewElement?.extent?.xmin, (xmin) => { console.log(`Extent change xmin = ${xmin}`); });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();
ResourceHandles and Promises
The watch(), on() and
when() functions return a ResourceHandle.
Handles retain references to observed objects and callbacks until they are removed.
The once() and whenOnce() functions return a
Promise instead of a ResourceHandle and have the option to be cancelled via an AbortSignal. When cancelled, the Promise settles and is eligible for disposal.
watch() and when() have an optional once property in
their options parameter. When once: true is set, the returned ResourceHandle will automatically remove
itself after the first time the callback is executed, which can be useful for one-time conditions
while still allowing for manual removal if needed.
Read More
To avoid memory leaks:
- Remove handles when the reference is no longer needed, such as during component teardown or before creating a replacement handle.
- Use Handles to manage groups of handles.
- Abort unresolved async operations, this allows them to be disposed.
Removing a handle stops the observation and releases references to observed objects and callbacks, allowing them to be garbage collected. Handle removal is idempotent and can be safely called multiple times.
// Remove a ResourceHandle to stop watching for changesconst handle = reactiveUtils.watch( () => viewElement?.extent?.xmin, (xmin) => { console.log(`Extent change xmin = ${xmin}`) });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove()Use Handles to group multiple ResourceHandles and remove them together, such as when a component is torn down. This also makes it easier to manage handles by name.
const handles = new Handles();handles.add( reactiveUtils.watch(...), "view-watcher" // groupKey is optional but can be helpful for managing handles);
// In another function or during component teardownhandles.remove("view-watcher");The once() and whenOnce() functions return a Promise.
In some advanced use cases where an action may take additional time, these
functions also offer the option to cancel the async callback via an AbortSignal.
If the component being watched is removed before the promise resolves, it can lead to memory leaks or unhandled promise rejections.
let abortController = new AbortController();const { signal } = abortController;
// Cancel the async callback by sending an AbortSignalconst abort = () => { abortController?.abort();}
// Set the signal property to watch for abort signals// and reject the promise if the signal is aborted before the promise resolvesreactiveUtils.whenOnce( () => !viewElement.updating, { signal }).then((updating) => { console.log("Map is not updating", updating);}).catch((error) => { if (error.name === "AbortError") { console.log("The async callback was aborted"); } else { console.error("An unexpected error occurred:", error); }}).finally(() => { console.log("Async callback has completed or was aborted"); abortController = null;});See the SDK guide topic on Async cancellation with AbortController for more details and examples.
Working with TypeScript
reactiveUtils also works with TypeScript. TypeScript infers the expression result type from the getValue
expression and passes it to the callback.
For array expressions, use explicit tuple typing when positional meaning matters.
Using as const preserves tuple types so callback parameters are strongly typed by position.
You can also use objects to group values and maintain strong typing without relying on position.
Read More
The first snippet shows how to use as const for tuple typing when tracking multiple properties in an array.
import { watch } from "@arcgis/core/core/reactiveUtils.js";import type { ArcgisMap } from "@arcgis/map-components/components/arcgis-map";
// Get a reference to the map componentconst arcgisMap = document.querySelector<ArcgisMap>("arcgis-map");
const handle = reactiveUtils.watch( () => [view.stationary, view.zoom] as const, ([stationary, zoom], oldValue) => { if (stationary) { const previousZoom = oldValue?.[1]; console.log("Zoom:", zoom, "Previous:", previousZoom); } });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();The second snippet shows how to use an object to group values and maintain strong typing without
relying on position. One advantage of this approach is the callback properties are named.
The order of properties in the getValue expression doesn't matter. This improves maintainability
and readability, especially when tracking multiple properties or when the expression is complex
because the properties are matched by the name and not position.
const handle = reactiveUtils.watch( () => ({ scale: viewElement.scale, stationary: viewElement.stationary, extent: viewElement.extent }), ({ stationary, extent, scale }) => { if (stationary) { console.log("View stopped moving"); console.log("Scale:", scale); console.log("Extent:", extent); } });If the getValue expression may return null or undefined,
shape the expression so the callback only runs when a defined value is available.
This avoids unnecessary callback executions and ensures TypeScript receives a narrowed type.
const handle = reactiveUtils.when( () => viewElement.popupElement?.selectedFeature?.geometry, (geometry) => { console.log(geometry.type); });
// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();Type definitions
ReactiveWatchOptions
- Type parameters
- <T = unknown>
Options used to configure how auto-tracking is performed and how the callback should be called.
ReactiveEqualityFunction
- Type parameters
- <T>
Function used to check whether two values are the same, in which case the watch callback isn't called.
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| newValue | T | The new value. | |
| oldValue | T | The old value. | |
- Returns
- boolean
Whether the new value is equal to the old value.
ReactiveWatchExpression
- Type parameters
- <T>
Expression which is auto-tracked and should return a value to pass to the ReactiveWatchCallback.
- Returns
- T
The new value.
ReactiveOnExpression
- Type parameters
- <T>
Expression which is auto-tracked and should return an event target to which an event listener is to be added.
- Returns
- T
The event target.
ReactiveWatchCallback
- Type parameters
- <T>
Function to be called when a value changes.
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| newValue | T | The new value. | |
| oldValue | T | The old value. | |
- Returns
- void
ReactiveOnCallback
- Type parameters
- <T>
Function to be called when an event is emitted or dispatched.
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| event | T | The event emitted by the target. | |
- Returns
- void
ReactiveListenerChangeCallback
- Type parameters
- <T>
Callback to be called when an event listener is added or removed.
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| target | T | The event target to which the listener was added or from which it was removed. | |
- Returns
- void
ReactiveOnOptions
- Type parameters
- <Target>
Options used to configure the behavior of on().
Functions
watch
- Type parameters
- <T, U extends T>
Tracks any properties accessed in the getValue expression and calls the callback
when any of them change.
- Signature
-
watch <T, U extends T>(getValue: ReactiveWatchExpression<T>, callback: ReactiveWatchCallback<T>, options?: ReactiveWatchOptions<U>): ResourceHandle
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| getValue | Function used to get the current value. All accessed properties will be tracked. | | |
| callback | The function to call when there are changes. | | |
| options | Options used to configure how the tracking happens and how the callback is to be called. | |
- Returns
- ResourceHandle
A watch handle.
- Examples
- // Watching for changes in a boolean valueconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.watch(() => viewElement.popupElement.open,() => {console.log(`Popup open: ${viewElement.popupElement.open}`);});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();// Watching for changes within a Collectionconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.watch(() => viewElement.map.allLayers.length,() => {console.log(`Layer collection length changed: ${viewElement.map.allLayers.length}`);});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();// Watch for changes in a numerical value.// Providing `initial: true` in ReactiveWatchOptions// checks immediately after initializationconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.watch(() => viewElement.zoom,() => {console.log(`zoom changed to ${viewElement.zoom}`);},{initial: true});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();// Watch properties from multiple sourcesconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.watch(() => [viewElement.stationary, viewElement.zoom],([stationary, zoom]) => {// Only print the new zoom value when the map component is stationaryif(stationary){console.log(`Change in zoom level: ${zoom}`);}});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();
when
- Type parameters
- <T, U extends T>
Watches the value returned by the getValue expression and calls the callback when it becomes truthy.
- Signature
-
when <T, U extends T>(getValue: ReactiveWatchExpression<T | null | undefined>, callback: (newValue: T, oldValue?: T) => void, options?: ReactiveWatchOptions<U>): ResourceHandle
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| getValue | ReactiveWatchExpression<T | null | undefined> | Expression used to get the current value. All accessed properties will be tracked. | |
| callback | (newValue: T, oldValue?: T) => void | The function to call when the value becomes truthy. | |
| options | Options used to configure how the tracking happens and how the callback is to be called. | |
- Returns
- ResourceHandle
A watch handle.
- Examples
- // Observe when a boolean property becomes not truthyconst handle = reactiveUtils.when(() => !layerView.updating,() => {console.log("LayerView finished updating.");});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();// Observe when a boolean property becomes trueconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.when(() => viewElement?.stationary === true,async () => {console.log("User is no longer interacting with the map");await drawBuffer();});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();// Observe a boolean property for truthiness.// Providing `once: true` in ReactiveWatchOptions// only fires the callback onceconst featuresComponent = document.querySelector("arcgis-features");const handle = reactiveUtils.when(() => featuresComponent.open,() => {console.log("The features component is open");},{once: true});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();
on
- Type parameters
- <Target extends EventTarget | EventedMixin>
Watches the value returned by the getTarget function for changes and
automatically adds or removes an event listener for a given event, as
needed.
- Signature
-
on <Target extends EventTarget | EventedMixin>(getTarget: ReactiveOnExpression<Target | null | undefined>, eventName: string, callback: ReactiveOnCallback<any>, options?: ReactiveOnOptions<Target>): ResourceHandle
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| getTarget | ReactiveOnExpression<Target | null | undefined> | Function which returns the object to which the event listener is to be added. | |
| eventName | The name of the event to add a listener for. | | |
| callback | The event handler callback function. | | |
| options | Options used to configure how the tracking happens and how the callback is to be called. | |
- Returns
- ResourceHandle
A watch handle.
- Examples
- // Adds a click event on a map component when it changesconst viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.on(() => viewElement,"arcgisViewClick",(event) => {console.log("arcgisViewClick event emitted: ", event);});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();// Adds a drag event on a map component and adds a callback// to check when the listener is added and removed.// Providing `once: true` in the ReactiveListenerOptions// removes the event after first callback.const viewElement = document.querySelector("arcgis-map");const handle = reactiveUtils.on(() => viewElement,"arcgisViewDrag",(event) => {console.log(`Drag event emitted: ${event}`);},{once: true,onListenerAdd: () => console.log("Drag listener added!"),onListenerRemove: () => console.log("Drag listener removed!")});// Remove the handle when it's no longer needed to stop watching for changeshandle.remove();
once
- Type parameters
- <T>
Tracks any properties being evaluated by the getValue expression. When getValue changes, it
returns a promise containing the value. This method only tracks a single change.
- Signature
-
once <T>(getValue: ReactiveWatchExpression<T>, signal?: AbortSignal | AbortOptions | null | undefined): Promise<T>
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| getValue | Expression to be tracked. | | |
| signal | AbortSignal | AbortOptions | null | undefined | Abort signal which can be used to cancel the promise from resolving. | |
- Returns
- Promise
A promise which resolves when the tracked expression changes.
- Examples
- // Observe the first time a property equals a specific string valuereactiveUtils.once(() => featureLayer.loadStatus === "loaded").then(() => {console.log("featureLayer loadStatus is loaded.");});// Use a comparison operator to resolve on the first tracked change that// matches this expression.const viewElement = document.querySelector("arcgis-map");const someFunction = async () => {await reactiveUtils.once(() => viewElement.zoom > 20);console.log("Zoom level is greater than 20!");}// Use a comparison operator and optional chaining to observe a// first time difference in numerical values.reactiveUtils.once(() => map?.allLayers?.length > 2).then((value) => {console.log(`The map now has ${value} layers.`);});
whenOnce
- Type parameters
- <T>
Tracks any properties being evaluated by the getValue expression. When getValue becomes truthy,
it returns a promise containing the value. This method only tracks a single change.
- Signature
-
whenOnce <T>(getValue: ReactiveWatchExpression<T | null | undefined>, signal?: (AbortSignal | AbortOptions) | null | undefined): Promise<T>
Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| getValue | ReactiveWatchExpression<T | null | undefined> | Expression to be tracked. | |
| signal | (AbortSignal | AbortOptions) | null | undefined | Abort signal which can be used to cancel the promise from resolving. | |
- Returns
- Promise
A promise which resolves once the tracked expression becomes truthy.
- Examples
- // Check for the first time a property becomes truthyconst viewElement = document.querySelector("arcgis-map");reactiveUtils.whenOnce(() => viewElement.popupElement.open).then(() => {console.log("Popup used for the first time");});// Check for the first time a property becomes not truthyconst someFunction = async () => {await reactiveUtils.whenOnce(() => !layerView.updating);console.log("LayerView is no longer updating");}// Check for the first time a property becomes truthy// And, use AbortController to cancel the async callbackconst abortController = new AbortController();const viewElement = document.querySelector("arcgis-map");// Observe the map component's updating state// The updating property is false by defaultreactiveUtils.whenOnce(() => viewElement?.updating, {signal: abortController.signal}).then((updating) => {console.log(`Map component updated.`)});// Cancel the async callbackconst someFunction = () => {abortController.abort();}