| name | arcgis-core-utilities |
| description | Core utilities including Accessor pattern, Collection, reactiveUtils, promiseUtils, esriRequest, intl (internationalization), and workers. Use for reactive programming, property watching, HTTP requests, locale formatting, async operations, and background processing. |
ArcGIS Core Utilities
Use this skill for core infrastructure patterns like Accessor, Collection, reactive utilities, and workers.
Import Patterns
Direct ESM Imports
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
import Collection from "@arcgis/core/core/Collection.js";
import Handles from "@arcgis/core/core/Handles.js";
Dynamic Imports (CDN)
const reactiveUtils = await $arcgis.import(
"@arcgis/core/core/reactiveUtils.js",
);
const promiseUtils = await $arcgis.import("@arcgis/core/core/promiseUtils.js");
Accessor Pattern
The Accessor class is the foundation for all ArcGIS classes, providing property watching and computed properties.
Creating Custom Accessor Classes
import Accessor from "@arcgis/core/core/Accessor.js";
import {
subclass,
property,
} from "@arcgis/core/core/accessorSupport/decorators.js";
@subclass("myapp.CustomClass")
class CustomClass extends Accessor {
@property()
name = "default";
@property()
value = 0;
@property({
readOnly: true,
dependsOn: ["name", "value"],
})
get summary() {
return `${this.name}: ${this.value}`;
}
}
const instance = new CustomClass({ name: "Test", value: 42 });
console.log(instance.summary);
Property Decorators
@subclass("myapp.MyClass")
class MyClass extends Accessor {
@property()
title = "";
@property({ type: Number })
count = 0;
@property({ readOnly: true })
get displayName() {
return this.title.toUpperCase();
}
@property()
get status() {
return this._status;
}
set status(value) {
this._status = value?.toLowerCase() || "unknown";
}
@property({
readOnly: true,
dependsOn: ["count", "title"],
})
get info() {
return `${this.title} (${this.count})`;
}
}
Property Types
@property({ type: String })
name = "";
@property({ type: Number })
count = 0;
@property({ type: Boolean })
enabled = true;
@property({ type: Date })
createdAt = new Date();
@property({ type: [Number] })
values = [];
@property({ type: SomeClass })
child = null;
Collection
Collection is an array-like class with change notifications.
Basic Usage
import Collection from "@arcgis/core/core/Collection.js";
const collection = new Collection([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
]);
collection.add({ id: 3, name: "Item 3" });
collection.addMany([
{ id: 4, name: "Item 4" },
{ id: 5, name: "Item 5" },
]);
collection.remove(item);
collection.removeAt(0);
collection.removeMany([item1, item2]);
collection.removeAll();
const first = collection.at(0);
const all = collection.toArray();
const length = collection.length;
Collection Methods
const found = collection.find((item) => item.id === 3);
const index = collection.findIndex((item) => item.name === "Item 2");
const filtered = collection.filter((item) => item.value > 10);
const mapped = collection.map((item) => item.name);
const reduced = collection.reduce((acc, item) => acc + item.value, 0);
const hasItem = collection.includes(item);
const exists = collection.some((item) => item.active);
const allActive = collection.every((item) => item.active);
collection.sort((a, b) => a.name.localeCompare(b.name));
collection.reverse();
collection.reorder(item, 0);
collection.push(item);
collection.pop();
collection.unshift(item);
collection.shift();
collection.splice(1, 2);
collection.forEach((item) => console.log(item.name));
for (const item of collection) {
console.log(item);
}
const featureLayers = map.layers.ofType(FeatureLayer);
Collection Events
collection.on("change", (event) => {
console.log("Added:", event.added);
console.log("Removed:", event.removed);
console.log("Moved:", event.moved);
});
collection.watch("length", (newLength, oldLength) => {
console.log(`Length changed from ${oldLength} to ${newLength}`);
});
Typed Collections
import Graphic from "@arcgis/core/Graphic.js";
const graphics = new Collection();
graphics.addMany([
new Graphic({ geometry: point1 }),
new Graphic({ geometry: point2 }),
]);
map.layers.add(featureLayer);
map.layers.reorder(featureLayer, 0);
reactiveUtils
Modern reactive utilities for watching properties and state changes.
watch()
Watch a property for changes.
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
const handle = reactiveUtils.watch(
() => view.scale,
(scale) => {
console.log("Scale changed to:", scale);
},
);
reactiveUtils.watch(
() => view.extent,
(extent) => console.log("Extent:", extent),
{ initial: true },
);
handle.remove();
when()
Wait for a condition to become true.
await reactiveUtils.when(
() => view.ready,
() => console.log("View is ready!"),
);
await reactiveUtils.when(() => layer.loaded);
console.log("Layer loaded");
await reactiveUtils.when(() => view.stationary, { timeout: 5000 });
once()
Watch for a value change only once.
await reactiveUtils.once(() => view.ready);
console.log("View became ready");
reactiveUtils.once(
() => layer.visible,
(visible) => console.log("Layer visibility changed to:", visible),
);
whenOnce()
Wait for a property to become truthy once, returns a promise.
await reactiveUtils.whenOnce(() => view.ready);
console.log("View is ready");
on()
Listen to events reactively.
const handle = reactiveUtils.on(
() => view,
"click",
(event) => {
console.log("Clicked at:", event.mapPoint);
},
);
reactiveUtils.on(
() => view.whenLayerView(layer),
"highlight",
(event) => console.log("Highlight changed"),
);
Multiple Properties
reactiveUtils.watch(
() => [view.center, view.zoom],
([center, zoom]) => {
console.log(`Center: ${center.x}, ${center.y}, Zoom: ${zoom}`);
},
);
reactiveUtils.watch(
() => view.scale < 50000,
(isZoomedIn) => {
layer.visible = isZoomedIn;
},
);
Sync Option
reactiveUtils.watch(
() => view.extent,
(extent) => updateUI(extent),
{ sync: true },
);
Handles
Manage multiple watch handles for cleanup.
import Handles from "@arcgis/core/core/Handles.js";
const handles = new Handles();
handles.add(
reactiveUtils.watch(
() => view.scale,
(scale) => {},
),
);
handles.add(
view.on("click", (e) => {}),
"click-handlers",
);
handles.add(
[
reactiveUtils.watch(
() => view.center,
() => {},
),
reactiveUtils.watch(
() => view.zoom,
() => {},
),
],
"view-watchers",
);
handles.remove("click-handlers");
handles.has("view-watchers");
handles.removeAll();
handles.destroy();
In Custom Classes
@subclass("myapp.MyWidget")
class MyWidget extends Accessor {
constructor(props) {
super(props);
this.handles = new Handles();
}
initialize() {
this.handles.add(
reactiveUtils.watch(
() => this.view?.scale,
(scale) => this.onScaleChange(scale),
),
);
}
destroy() {
this.handles.destroy();
super.destroy();
}
}
Using addHandles on Accessor
All Accessor-based classes have built-in handle management:
view.addHandles(
reactiveUtils.watch(
() => view.scale,
(scale) => {},
),
"my-group",
);
view.hasHandles("my-group");
view.removeHandles("my-group");
promiseUtils
Utilities for working with Promises.
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
eachAlways()
Wait for all promises, regardless of success/failure.
const results = await promiseUtils.eachAlways([
fetch("/api/data1"),
fetch("/api/data2"),
fetch("/api/data3"),
]);
results.forEach((result, index) => {
if (result.error) {
console.error(`Request ${index} failed:`, result.error);
} else {
console.log(`Request ${index} succeeded:`, result.value);
}
});
debounce()
Debounce a function.
const debouncedSearch = promiseUtils.debounce(async (query) => {
const results = await searchService.search(query);
return results;
});
input.addEventListener("input", async (e) => {
try {
const results = await debouncedSearch(e.target.value);
displayResults(results);
} catch (e) {
if (!promiseUtils.isAbortError(e)) {
console.error(e);
}
}
});
isAbortError()
Check if error is from aborted operation.
try {
await someAsyncOperation();
} catch (error) {
if (promiseUtils.isAbortError(error)) {
console.log("Operation was cancelled");
} else {
throw error;
}
}
ignoreAbortErrors()
Suppress abort errors from a promise.
await promiseUtils.ignoreAbortErrors(debouncedFunction());
createAbortError()
Create an abort error for use with AbortSignal.
const error = promiseUtils.createAbortError();
filter()
Async filter of arrays.
const validItems = await promiseUtils.filter(items, async (item) => {
const result = await validateItem(item);
return result.isValid;
});
Workers
Run heavy computations in background threads.
import * as workers from "@arcgis/core/core/workers.js";
Open Worker Connection
const connection = await workers.open("path/to/worker.js");
const result = await connection.invoke("methodName", { data: "params" });
connection.close();
Worker Script Example
define([], function () {
return {
methodName: function (params) {
const result = processData(params.data);
return result;
},
anotherMethod: function (params) {
return params.value * 2;
},
};
});
Using Workers for Heavy Tasks
import * as workers from "@arcgis/core/core/workers.js";
async function processLargeDataset(data) {
const connection = await workers.open("./dataProcessor.js");
try {
const result = await connection.invoke("process", {
data: data,
options: { threshold: 100 },
});
return result;
} finally {
connection.close();
}
}
Error Handling
import Error from "@arcgis/core/core/Error.js";
const error = new Error("layer-load-error", "Failed to load layer", {
layer: layer,
originalError: e,
});
if (error.name === "layer-load-error") {
console.log("Layer failed:", error.details.layer.title);
}
URL Utilities
import * as urlUtils from "@arcgis/core/core/urlUtils.js";
urlUtils.addProxyRule({
urlPrefix: "https://services.arcgis.com",
proxyUrl: "/proxy",
});
esriRequest
HTTP client bundled with the SDK. Handles ArcGIS auth, proxy rules, and CORS automatically. Use it for anything that hits an ArcGIS REST endpoint and for generic HTTP where you want SDK-level auth/proxy handling.
Basic Request
import esriRequest from "@arcgis/core/request.js";
const response = await esriRequest(url, {
query: { f: "json" },
responseType: "json",
});
console.log("Status:", response.httpStatus);
console.log("Data:", response.data);
Request with Options
const response = await esriRequest(url, {
query: {
f: "json",
param1: "value1",
},
responseType: "json",
method: "post",
body: formData,
timeout: 30000,
headers: {
"X-Custom-Header": "value",
},
});
Download Binary Data
const imageResponse = await esriRequest(imageUrl, {
responseType: "image",
});
const imageElement = imageResponse.data;
const binaryResponse = await esriRequest(fileUrl, {
responseType: "array-buffer",
});
const arrayBuffer = binaryResponse.data;
Abortable Requests
Pair with an AbortController for cancellation — especially important for custom layers and debounced workflows. Use promiseUtils.isAbortError to distinguish user-cancelled from real errors.
import esriRequest from "@arcgis/core/request.js";
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
const controller = new AbortController();
try {
const response = await esriRequest(url, {
responseType: "json",
signal: controller.signal,
});
} catch (error) {
if (promiseUtils.isAbortError(error)) {
} else {
throw error;
}
}
controller.abort();
Scheduling
import * as scheduling from "@arcgis/core/core/scheduling.js";
const handle = scheduling.addFrameTask({
update: (event) => {
console.log("Delta time:", event.deltaTime);
console.log("Elapsed:", event.elapsedTime);
},
});
handle.pause();
handle.resume();
handle.remove();
scheduling.schedule(() => {
console.log("Executed on next frame");
});
Common Patterns
Cleanup Pattern
class MyComponent {
constructor(view) {
this.view = view;
this.handles = new Handles();
this.setup();
}
setup() {
this.handles.add([
reactiveUtils.watch(
() => this.view.scale,
(scale) => this.onScaleChange(scale),
),
reactiveUtils.watch(
() => this.view.extent,
(extent) => this.onExtentChange(extent),
),
this.view.on("click", (e) => this.onClick(e)),
]);
}
destroy() {
this.handles.removeAll();
}
}
Debounced Updates
const debouncedUpdate = promiseUtils.debounce(async () => {
const extent = view.extent;
const results = await layer.queryFeatures({
geometry: extent,
returnGeometry: true,
});
updateDisplay(results);
});
reactiveUtils.watch(() => view.stationary && view.extent, debouncedUpdate);
Conditional Watching
reactiveUtils.watch(
() => (view.stationary ? view.extent : null),
(extent) => {
if (extent) {
updateForExtent(extent);
}
},
);
Internationalization (intl)
The intl module provides locale-aware formatting for numbers, dates, and coordinates.
Number Formatting
import * as intl from "@arcgis/core/intl.js";
const formatted = intl.formatNumber(1234567.89);
const currency = intl.formatNumber(1234.5, {
style: "currency",
currency: "USD",
});
const percent = intl.formatNumber(0.75, {
style: "percent",
});
Date Formatting
import * as intl from "@arcgis/core/intl.js";
const date = new Date();
const formatted = intl.formatDate(date);
const longDate = intl.formatDate(date, {
dateStyle: "full",
timeStyle: "short",
});
const dateOnly = intl.formatDateOnly("2024-03-15");
const timeOnly = intl.formatTimeOnly("14:30:00");
const timestamp = intl.formatTimestamp("2024-03-15T14:30:00Z");
Setting Locale
import * as intl from "@arcgis/core/intl.js";
const locale = intl.getLocale();
const lang = intl.getLocaleLanguage();
const normalized = intl.normalizeMessageBundleLocale("en-us");
Message Bundles (i18n)
import * as intl from "@arcgis/core/intl.js";
const loader = intl.createJSONLoader({
pattern: "my-app/",
base: "./nls",
});
const messages = await intl.fetchMessageBundle("my-app/messages");
console.log(messages.greeting);
Format Conversion Utilities
import * as intl from "@arcgis/core/intl.js";
const intlOptions = intl.convertDateFormatToIntlOptions(
"short-date-short-time",
);
const numberOptions = intl.convertNumberFieldFormatToIntlOptions(fieldFormat);
const dateOptions =
intl.convertDateTimeFieldFormatToIntlOptions(dateFieldFormat);
Coordinate Formatting
import * as coordinateFormatter from "@arcgis/core/geometry/coordinateFormatter.js";
await coordinateFormatter.load();
const dms = coordinateFormatter.toLatitudeLongitude(point, "dms", 2);
const utm = coordinateFormatter.toUtm(point, "north-south-indicators", true);
const parsed = coordinateFormatter.fromLatitudeLongitude("34.02N 118.805W");
Reference Samples
watch-for-changes - Watching property changes on map objects
watch-for-changes-reactiveutils - Modern reactive property watching
chaining-promises - Promise chaining patterns with the API
event-explorer - Exploring view and layer events
Related Skills
- See
arcgis-core-maps for map and view setup that uses reactiveUtils.
- See
arcgis-layers for layer management using Collection.
- See
arcgis-interaction for event handling patterns.
Common Pitfalls
-
Memory Leaks: Always remove handles when done.
reactiveUtils.watch(
() => view.scale,
(scale) => updateUI(scale),
);
const handle = reactiveUtils.watch(
() => view.scale,
(scale) => updateUI(scale),
);
handle.remove();
Impact: Watchers accumulate over time, causing performance degradation and potential errors if they reference destroyed objects.
-
Initial Value: Use initial: true to get current value immediately.
reactiveUtils.watch(
() => view.scale,
(scale) => updateUI(scale),
);
reactiveUtils.watch(
() => view.scale,
(scale) => updateUI(scale),
{ initial: true },
);
Impact: UI is not initialized with the current value, remaining empty or stale until the first property change.
-
Sync vs Async: Default is async batching, use sync: true carefully.
reactiveUtils.watch(() => value, callback);
reactiveUtils.watch(() => value, callback, { sync: true });
Impact: Sync watchers fire on every single change, which can cause layout thrashing and poor performance if the watched property updates frequently.
-
Abort Errors: Always check for abort errors in catch blocks.
try {
await debouncedFunction();
} catch (e) {
console.error("Error!", e);
}
try {
await debouncedFunction();
} catch (e) {
if (!promiseUtils.isAbortError(e)) {
console.error("Error!", e);
}
}
Impact: Abort errors (from debounced operations, cancelled queries, or navigation changes) are logged as real errors, flooding the console and potentially triggering error-reporting systems.