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.
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:
- Stores the view and map in
mapStoreas opaque references. - Sets
view.constraints(below). 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.- Re-injects the stream token over every catalogued layer.
- Registers the layer click handler, the right-click
contextmenulistener and the touchholdhandler for the coordinates card, and passes the legend itslayerInfos. 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.- 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. - Fits California when there is no persisted position, applies touch navigation options on mobile, and moves the Alerts layer above the FOV layer.
- 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.