import { watch, when, on, once, whenOnce } from "@arcgis/core/core/reactiveUtils.js";
Since
ArcGIS Maps SDK for JavaScript 4.23

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:

FunctionRuns multiple timesResolves onceReturnsCommon use
watch()YesNoResourceHandleContinuously observe for changes
when()YesNoResourceHandleRun when expression becomes truthy
on()YesNoResourceHandleObserve events
once()NoYesPromiseWait for first emitted value
whenOnce()NoYesPromiseWait 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 property
const 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 changes
handle.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 visibility

Basic usage rules:

  • Shape expressions so callbacks receive defined values.
  • Avoid truthy checks when falsy values like 0, "", or false are valid.
  • Use explicit comparisons for specific values (e.g., === 0 or === 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.

GoalRecommended approach
Wait for property existenceTruthy
Wait for async resourceTruthy
Check boolean state (true or false)Explicit
Allow values such 0 or ""Explicit
Distinguish null or undefinedExplicit
Implement exact application logicExplicit
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 changes
handle.remove();

Other truthy examples include:

() => viewElement.ready
() => layer.loaded
() => layer.visible

Use 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 changes
handle.remove();

Other examples include:

() => viewElement.updating === false
() => layer.visible === false

See 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 changes
handle.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 changes
handle.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 changes
handle.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 changes
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 changes
handle.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 teardown
handles.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 AbortSignal
const abort = () => {
abortController?.abort();
}
// Set the signal property to watch for abort signals
// and reject the promise if the signal is aborted before the promise resolves
reactiveUtils.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 component
const 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 changes
handle.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 changes
handle.remove();
See also

Type definitions

ReactiveWatchOptions

Type definition
Type parameters
<T = unknown>

Options used to configure how auto-tracking is performed and how the callback should be called.

initial

readonly Property
Type
boolean | undefined

Whether to fire the callback immediately after initialization, if the necessary conditions are met.

Default value
false

sync

readonly Property
Type
boolean | undefined

Whether to fire the callback synchronously or on the next microtask.

Default value
false

once

readonly Property
Type
boolean | undefined

Whether to fire the callback only once.

Default value
false

equals

readonly Property
Type
ReactiveEqualityFunction | undefined

Function used to check whether two values are the same, in which case the callback isn't called. Checks whether two objects, arrays or primitive values are shallow equal, e.g. one level deep. Non-plain objects are considered equal if they are strictly equal (===).

ReactiveEqualityFunction

Type definition
Type parameters
<T>

Function used to check whether two values are the same, in which case the watch callback isn't called.

Parameters

ParameterTypeDescriptionRequired
newValue
T

The new value.

oldValue
T

The old value.

Returns
boolean

Whether the new value is equal to the old value.

ReactiveWatchExpression

Type definition
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 definition
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 definition
Type parameters
<T>

Function to be called when a value changes.

Parameters

ParameterTypeDescriptionRequired
newValue
T

The new value.

oldValue
T

The old value.

Returns
void

ReactiveOnCallback

Type definition
Type parameters
<T>

Function to be called when an event is emitted or dispatched.

Parameters

ParameterTypeDescriptionRequired
event
T

The event emitted by the target.

Returns
void

ReactiveListenerChangeCallback

Type definition
Type parameters
<T>

Callback to be called when an event listener is added or removed.

Parameters

ParameterTypeDescriptionRequired
target
T

The event target to which the listener was added or from which it was removed.

Returns
void

ReactiveOnOptions

Type definition
Type parameters
<Target>

Options used to configure the behavior of on().

sync

Property
Type
boolean | undefined

Whether to fire the callback synchronously or on the next microtask.

Default value
false

once

Property
Type
boolean | undefined

Whether to fire the callback only once.

Default value
false

onListenerAdd

Property
Type
ReactiveListenerChangeCallback | undefined

Called when the event listener is added.

onListenerRemove

Property
Type
ReactiveListenerChangeCallback | undefined

Called when the event listener is removed.

Functions

watch

Function
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

ParameterTypeDescriptionRequired
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 value
const 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 changes
handle.remove();
// Watching for changes within a Collection
const 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 changes
handle.remove();
// Watch for changes in a numerical value.
// Providing `initial: true` in ReactiveWatchOptions
// checks immediately after initialization
const 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 changes
handle.remove();
// Watch properties from multiple sources
const 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 stationary
if(stationary){
console.log(`Change in zoom level: ${zoom}`);
}
}
);
// Remove the handle when it's no longer needed to stop watching for changes
handle.remove();

when

Function
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

ParameterTypeDescriptionRequired
getValue

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 truthy
const handle = reactiveUtils.when(
() => !layerView.updating,
() => {
console.log("LayerView finished updating.");
});
// Remove the handle when it's no longer needed to stop watching for changes
handle.remove();
// Observe when a boolean property becomes true
const 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 changes
handle.remove();
// Observe a boolean property for truthiness.
// Providing `once: true` in ReactiveWatchOptions
// only fires the callback once
const 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 changes
handle.remove();

on

Function
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

ParameterTypeDescriptionRequired
getTarget

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 changes
const 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 changes
handle.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 changes
handle.remove();

once

Function
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

ParameterTypeDescriptionRequired
getValue

Expression to be tracked.

signal

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 value
reactiveUtils.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

Function
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

ParameterTypeDescriptionRequired
getValue

Expression to be tracked.

signal

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 truthy
const 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 truthy
const 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 callback
const abortController = new AbortController();
const viewElement = document.querySelector("arcgis-map");
// Observe the map component's updating state
// The updating property is false by default
reactiveUtils.whenOnce(
() => viewElement?.updating, {signal: abortController.signal})
.then((updating) => {
console.log(`Map component updated.`)
});
// Cancel the async callback
const someFunction = () => {
abortController.abort();
}