Part of the Dashboard State & URL Sharing guide.
Operative rule: encode the difference from the default layer set, not the full set — a link then stays short, survives new layers being added, and says what the reader actually decided.
Diff Encoding Beats a Full List
A dashboard with twelve layers has twelve default visibility states. Encoding all of them into every link produces long, noisy URLs where eleven tokens are just restating the default. Worse, when a thirteenth layer ships, every previously shared link becomes a statement that the new layer should be off — because the link enumerated a world in which it did not exist.
Encoding only the changes fixes both. A reader who turned one layer on shares l=+flood; a reader who turned two off shares l=-roads,-labels. New layers inherit their default, old links keep working, and the URL says what the reader did rather than restating the design.
Production-Ready Implementation
// The registry is generated in Python and embedded in the page.
// { id, label, defaultVisible, group }
const REGISTRY = window.__LAYER_REGISTRY__;
export function encodeLayers(state) {
const tokens = [];
for (const layer of REGISTRY) {
const on = Boolean(state[layer.id]);
if (on === layer.defaultVisible) continue; // only the difference travels
tokens.push((on ? "+" : "-") + layer.id);
}
return tokens.sort().join(","); // sorted → canonical
}
export function decodeLayers(token) {
const state = Object.fromEntries(
REGISTRY.map((layer) => [layer.id, layer.defaultVisible])
);
const unknown = [];
for (const raw of (token || "").split(",").filter(Boolean)) {
const sign = raw[0];
const id = raw.slice(1);
if (!(id in state)) { unknown.push(id); continue; } // ignore, never throw
if (sign === "+") state[id] = true;
else if (sign === "-") state[id] = false;
}
if (unknown.length) {
console.info(`ignored unknown layer id(s): ${unknown.join(", ")}`);
}
return state;
}
// Apply BEFORE the first paint: build the style with the right layers already
// hidden, rather than adding them all and switching some off afterwards.
export function applyLayerState(styleDoc, state) {
for (const layer of styleDoc.layers) {
if (!(layer.id in state)) continue;
layer.layout = layer.layout || {};
layer.layout.visibility = state[layer.id] ? "visible" : "none";
}
return styleDoc;
}
Applying the state to the style document before the map is constructed is what stops the flash of layers switching off after load. It also avoids a subtler cost: a layer that is added and then immediately hidden has already requested its tiles, so the reader pays for data they never see.
Handling Groups and Mutually Exclusive Sets
Why Identifiers Are a Public Contract
The moment a layer id appears in a shared URL it stops being an internal name. Someone will paste that link into a ticket, a wiki page or an email, and it will be opened months later. Renaming the layer then silently changes what the link means — or, with the ignore-unknown behaviour above, silently drops it.
The practical discipline is to treat ids like any other published interface: choose them deliberately, keep them free of implementation detail, and when one must change, keep a mapping from the old name to the new one and apply it during decoding. That mapping is a dozen lines and it is the difference between links that last and links that quietly rot.
Verification Steps
- Encode the default state and confirm the token is empty.
- Toggle one layer, share, open in a clean session, and confirm exactly that layer differs.
- Add a new layer to the registry and confirm an old link leaves it at its default.
- Open a link naming a removed layer and confirm the rest of the state still applies.
- Confirm the layer state is applied to the style before construction, with no visible toggling after load.
Common Errors & Fixes
Shared links turn off layers the sender never touched
The full set is being encoded. Switch to the difference form so only real decisions travel.
The same view produces different URLs for different readers
Tokens are not sorted, or the state object’s key order is leaking into the output. Sort before joining.
A link opens with layers visible that the control shows as off
Two sources of truth: the style was updated but the registry state was not. Apply both from one call, as the parent guide’s layer management pattern requires.
Toggling a layer does not add a history entry
Visibility changes are being written with replaceState. Layer toggles are decisions and should push, so the back button undoes them.
Keeping the Control and the Map in Step
Restoring visibility touches two things: the map, and the control that claims to describe it. A restore that updates only the first produces a dashboard where a layer is visible and its checkbox is unticked — which readers report as “the filters do not work”, because from their side that is exactly what it looks like.
The arrangement that prevents it is the one the parent guide describes: a single state object that both the control and the renderer read from. Restoring then means writing the decoded state into that object once and letting both surfaces update from it. There is no ordering to get right and no second code path for “restored” versus “toggled”, which is where the discrepancy usually creeps in.
It is worth testing this explicitly, because the failure is invisible in the common case. A link that turns a layer on looks fine even when the control is out of step, since a reader who did not open the panel never notices. The failure shows up only when someone opens the layer list, sees a state that contradicts the map, and loses confidence in both.
Groups, Defaults and Links That Outlive a Redesign
Layer sets are not static across a dashboard’s life. Layers are added, retired, merged into groups and split out again — and every one of those changes has an effect on links already in circulation.
Adding a layer is safe with difference encoding: old links do not mention it, so it takes its default. Retiring one is safe too, provided unknown identifiers are ignored rather than treated as errors. The two changes that need care are merging and splitting. Merging two layers into a group means old links referencing either member should now reference the group, which is what the alias map is for. Splitting a group into members means an old link referencing the group should expand to all of them — an alias entry that maps one identifier to several.
Neither is difficult, but both have to be considered at the moment of the change rather than afterwards, because there is no way to tell later which links were created before it. Keeping the alias map beside the layer registry, and updating both in the same commit, makes that a habit rather than an act of memory.
Gotchas & Edge Cases
- Layer identifiers that appear in links must never be reused for a different layer, however tempting the name; an old link then silently means something new.
- A mutually exclusive group needs exactly one active member after decoding, so a token naming a retired basemap must fall back to the default rather than leaving none selected.
- Ordering matters for the canonical form but not for application; sort on encode and ignore order on decode, so hand-edited links still work.
- Applying visibility to a style before construction requires the layer ids in the style to match the registry ids — a mismatch is silent, because setting visibility on an unknown layer is a no-op.
Related
- Dashboard State & URL Sharing — the parent guide covering the full state model
- Encoding Map Camera State in the URL Fragment — the other half of a shareable view
- Layer Management & Toggling — the registry these identifiers come from
- Building a Custom Layer Switcher in PyDeck — the same restore problem in a deck.gl stack