Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

Serial queues

One occupying layer at a time with scope serial, getQueuedSnapshot, and queued-confirm flows.

Onboarding steps, queued confirms, wizards — some surfaces must show one layer at a time. Set scope: { strategy: "serial" } on the stack so later opens wait as queued instead of mounting in parallel.

Configure serial scope

Per-stack at client creation or per-open via stack defaults:

const client = new LayerClient({
  defaultStackOptions: {
    confirm: { scope: { strategy: "serial" } },
  },
});

Or inline when ensuring a stack:

client.ensureStack("onboarding", { scope: { strategy: "serial" } });

Observe the queue

Active layers appear in getSnapshot(). Waiting layers are phase: "queued" and visible only via getQueuedSnapshot():

const stack = client.getStack("confirm");

stack.subscribe(() => {
  const active = stack.getSnapshot();
  const queued = stack.getQueuedSnapshot(); 
  console.log({ active: active.length, queued: queued.length });
});

Queued layers are not mounted — their components do not render until the occupying layer leaves.

Onboarding flow

const steps = [
  layerOptions({ stack: "onboarding", key: ["onboarding", 1], component: Step1 }),
  layerOptions({ stack: "onboarding", key: ["onboarding", 2], component: Step2 }),
  layerOptions({ stack: "onboarding", key: ["onboarding", 3], component: Step3 }),
];

for (const step of steps) {
  void client.open(step); 
  // only one occupying; rest queue
}

Each step’s component calls call.end() when done; the next queued layer activates automatically.

Queued confirm

Show queue position in UI by subscribing to both snapshots:

function QueueIndicator() {
  const client = useLayerClient();
  const stack = client.getStack("confirm");
  const [queued, setQueued] = useState(0);

  useEffect(() => {
    return stack.subscribe(() => {
      setQueued(stack.getQueuedSnapshot().length); 
    });
  }, [stack]);

  if (queued === 0) return null;
  return <span>{queued} waiting</span>;
}

In adapters, observe queued layers with useLayerQueuedState({ key, stack }) (per-key) or useQueuedStack({ stack }) (whole stack) — both return LayerState[] for phase: "queued" rows. Svelte names these createLayerQueuedState / createQueuedStack. Wired handles expose the same slice as .queued (useLayer / injectLayer / createLayer).

Cancel a queued layer

cancelQueued resolves and removes a queued layer without mounting — blockers do not apply. Omit { id } to drop the FIFO head for that key; pass { id: state.id } to target one queued instance:

const stack = client.getStack("confirm");

// FIFO head for this key
stack.cancelQueued(["confirm", "remove"], false);

// exact queued row
stack.cancelQueued(["confirm", "remove"], false, { id: queuedState.id }); 

On a wired handle, confirmLayer.cancelQueued(false) is FIFO for the bound key; confirmLayer.cancelQueued(false, { id }) targets one row.

Handle failed loads

By default (onLoadError: "block"), a rejecting loadFn leaves phase: "error" mounted. That layer still occupies the lane — queued work waits, and further opens queue — until you dismiss it from error UI or catch.

For a silent skip (remove the failed layer and drain the next queued open):

client.ensureStack("onboarding", {
  scope: { strategy: "serial", onLoadError: "advance" },
});

Narrow rejections in Error handling.

Edge cases

  • dismissAll drains the queue — queued callers resolve without mounting.
  • cancelQueued — see above; also on LayerHandle / useLayer(options).
  • Default scope is "parallel" — layers stack freely. See Stacks, scope, and gcTime.

Last updated on July 19, 2026

Was this page helpful?