Skip to main content

One WebMap, One View, One Lifetime

The first version of this app rendered a <FeatureMap /> in three places: the desktop map panel, the mobile tile-view drawer and the mobile location-view panel. Each mount created a MapView from scratch: WebMap download, a hundred layer views, every camera arrow, the stream authentication. Opening the bottom drawer on a phone re-did all of it, and closing it threw it away. The fix is the rule in this page's title. There is one view, it is created once, and it lives as long as the tab.

The singleton and its slots​

context/MapPortalContext.tsx renders FeatureMap exactly once, inside an always-mounted position: fixed overlay. Pages that want a map render an empty placeholder div and register it as a slot with useMapSlot(id); the provider copies the active slot's bounding rectangle onto the overlay's inline style, so the persistent map visually tracks whichever placeholder is on screen. Three slots exist, with a fixed priority when more than one is registered during a transition: mobile-location-map, mobile-drawer-map, desktop-map.

The obvious alternative, a React portal, does not work here, and the reason is worth knowing before you are tempted. <arcgis-map> is a Stencil custom element. Moving it to a new DOM parent, even through appendChild, runs the browser's node-removal algorithm first, which fires disconnectedCallback and then connectedCallback; Stencil uses exactly those to tear down and re-create the MapView. A portal reproduces the full re-initialisation through a different code path. So the map's DOM subtree is never touched by React after the first mount; only the overlay's top, left, width, height, visibility and z-index change.

Two refinements came from production. The map does not mount when a slot is merely registered; it waits for the first non-zero rectangle, because a MapView initialised against a 0 × 0 container never recovers its extent. And the overlay's z-index is recomputed on every sync as the maximum over the slot's positioned ancestors plus one, since the mobile drawer raises its own z-index each time it opens; caching the value once produced a map that was mounted, positioned and invisible behind the drawer's paper.

From token-ready to map-ready​

Press Play to follow the token from Key Vault into the browser, the WebMap prepared with its toggled-off layers parked, and the view-ready handler bringing the layers up until isMapReady.

Token — minted once, inlined, registered, refreshed Prepare & mount — load, park, render the element with a map object Layers → ready — three initialisations in parallel, then the gate
Node inspector

Click any node in the diagram for the real class, table, or entity behind that box.

Before the view exists. usePreparedWebMap constructs new WebMap({ portalItem }) and calls load(), which fetches the item JSON and instantiates every layer object without loading any of them. It snapshots the tree into a catalog, parks every toggleable layer, resets the per-layer load-status store, and injects the stream token. Only then does FeatureMap render <arcgis-map map={preparedWebMap.webmap}>. Handing the element a map object rather than an item-id is the whole trick: the view is created from a map that only contains the always-on layers, so those are all it loads. The next two pages cover what parking means.

The view-ready handler. handleArcgisViewReadyChange runs once, guarded by a ref, and does the work in a fixed order:

  1. Stores the view and map in mapStore as opaque references.
  2. Sets view.constraints (below).
  3. processLayers(webmap) — builds the Layers Menu tree, evicts auth-failed layers, disables native popups on the camera and alert layers, and forces the topo layer on and imagery off so the first basemap toggle is a real change.
  4. Re-injects the stream token over every catalogued layer.
  5. Registers the layer click handler, the right-click contextmenu listener and the touch hold handler for the coordinates card, and passes the legend its layerInfos.
  6. initializeLayers(webmap) runs three things in parallel: Velocity discovery (synchronous prelude, bounded async tail), creation of the four client layers, and location of the camera cursor-line layer; Velocity discovery publishes synchronously. None blocks another.
  7. Stores the camera layer with the clustering hook, builds the arrows from staticCameraStore (deferred 1.5 s on phones so the tile list renders first), registers the desktop camera click handler, and wires cluster hover.
  8. Fits California when there is no persisted position, applies touch navigation options on mobile, and moves the Alerts layer above the FOV layer.
  9. Flips isMapReady.

isMapReady is the gate for every hook: hover, tap and extent handlers are registered in an effect that depends on it, polling and selection take it as enabled, and the area filter only runs once it and its feature flag are both true.

Where the map starts​

The initial center and zoom-or-scale are captured once into a ref, so the element's attributes never fight later goTo() calls. The value comes from getSharedView(): the persisted mapOptions from the user's UI settings when present (preferring scale, which is continuous, over zoom, which was rounded to a whole level on save), else the selected camera at location-view zoom, else the California default.

A persisted "zoom" that is actually a scale is a save-side mistake we have seen, and goTo({ zoom: 500000 }) clamps silently to the deepest level instead of erroring. The resolveZoomOrScale helper applies a simple heuristic: a value at most 24 is a zoom level, anything larger is a scale.

Without a persisted position the view is fitted to a 4% margin around California's census extent, and re-fitted on every change to the view's size (split-pane drags, rotation, sidebar toggles) until the operator navigates themselves, which a reactiveUtils.watch on view.interacting records.

Constraints: where the map may go​

Five constraints are set once in the ready handler, and two of them were chosen together. geometry is a North America extent in Web Mercator that bounds the pan center to CONUS, Canada and a Mexico buffer; minScale of 70,000,000 stops the zoom-out before a world view, and the two have to agree because the extent limits only the center while the scale limits how far out the view may go. maxScale is 1,000 for a signed-in operator and 9,027.98 (zoom level 16) for a public visitor, because the site-hosting MOUs forbid street-level detail for anonymous use. Rotation is off so north stays up and bearings can be read off the map; snapToZoom is off so a wheel tick is a smooth step instead of a full level.

The public cap is re-applied whenever the session status flips after the map has initialised, because the singleton outlives a sign-in or sign-out. If the view is already past the new cap it is pulled back immediately rather than on the next interaction.

Teardown​

The cleanup effect removes markers and listeners, destroys the view and clears mapStore. In practice it runs only on a hard unload: the singleton is never unmounted during navigation, which is the point. If you ever see "FeatureMap initialized successfully" twice in one session, something has reparented the element.

Wrapping up​

One MapView, mounted once into a fixed overlay that follows a placeholder, never reparented. Boot is a fixed sequence: prepare the WebMap with its toggled-off layers parked, render the element with a map object, then in the ready handler constrain, curate, authenticate, create layers in parallel, build arrows, fit, and finally declare the map ready. Everything else in this chapter starts from that flag.

Next: Every Layer Has an Owner.