Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

Getting started

React quickstart — declare a layer, mount an outlet, open and await a typed result.

Stop threading isOpen through props — declare the contract once, mount an outlet, and open from any descendant. This guide uses React; other adapters: Install.

Install

npm install @stainless-code/react-layers
pnpm add @stainless-code/react-layers
yarn add @stainless-code/react-layers
bun add @stainless-code/react-layers

The core (@stainless-code/layers) is pulled in automatically and re-exported from the adapter.

Declare

Name the contract once — payload in, response out — with layerOptions:

import {
  layerOptions,
  type LayerComponentProps,
} from "@stainless-code/react-layers";

type ConfirmPayload = { title: string };
type ConfirmResponse = boolean;

function ConfirmDialog({
  call,
  payload,
}: LayerComponentProps<ConfirmPayload, ConfirmResponse>) {
  return (
    <div role="dialog">
      <h2>{payload.title}</h2>
      <button onClick={() => void call.end(true)}>Yes</button>
      <button onClick={() => void call.end(false)}>No</button>
    </div>
  );
}

const confirm = layerOptions<ConfirmPayload, ConfirmResponse>({ 
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});
import {
  layerOptions,
} from "@stainless-code/react-layers";

function ConfirmDialog({
  call,
  payload,
}) {
  return (
    <div role="dialog">
      <h2>{payload.title}</h2>
      <button onClick={() => void call.end(true)}>Yes</button>
      <button onClick={() => void call.end(false)}>No</button>
    </div>
  );
}

const confirm = layerOptions({ 
  stack: "confirm",
  key: ["confirm", "remove"],
  component: ConfirmDialog,
});

A typical layout keeps layer declarations in one folder so any component can import them:

  • src/
    • layers/
      • confirm.ts
      • toast.ts
    • App.tsx
    • main.tsx

Mount

Wrap your app with StackProvider and render active layers with StackOutlet:

import { StackProvider, StackOutlet } from "@stainless-code/react-layers";

function App() {
  return (
    <StackProvider>
      <StackOutlet stack="confirm" />
    </StackProvider>
  );
}
import { StackProvider, StackOutlet } from "@stainless-code/react-layers";

function App() {
  return (
    <StackProvider>
      <StackOutlet stack="confirm" />
    </StackProvider>
  );
}

Open and await

Wire the declaration with useLayer(options) — payload-only open, inferred R, plus reactive state/queued/top:

import { useLayer } from "@stainless-code/react-layers";

function RemoveButton() {
  const confirmLayer = useLayer(confirm);

  async function handleRemove() {
    const ok = await confirmLayer.open({ title: "Remove?" }); 
    //    ^? boolean
    if (ok) {
      /* … */
    }
  }

  return (
    <button type="button" onClick={() => void handleRemove()}>
      Remove
    </button>
  );
}
import { useLayer } from "@stainless-code/react-layers";

function RemoveButton() {
  const confirmLayer = useLayer(confirm);

  async function handleRemove() {
    const ok = await confirmLayer.open({ title: "Remove?" }); 
    //    ^? boolean
    if (ok) {
      /* … */
    }
  }

  return (
    <button type="button" onClick={() => void handleRemove()}>
      Remove
    </button>
  );
}

Bag-form stays valid when you already hold a client: await client.open({ ...confirm, payload }). See Glossary for drive vs observe and Migration for the escape hatch.

Optional payload and response — see Awaiting results.

Last updated on September 4, 2026

Was this page helpful?