Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

React

React adapter — StackProvider, useStack, StackOutlet, and ergonomic stack helpers.

@stainless-code/react-layers binds stack snapshots to React via useSyncExternalStore. The core is re-exported — import everything from one package.

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

Peer: react ^18.0.0 || ^19.0.0

Client

Wrap your tree with StackProvider (creates a LayerClient when omitted). Read it anywhere with useLayerClient:

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

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

Render a stack

StackOutlet maps the active snapshot to each layer’s registered component. For custom rendering, use useStackHandles or StackSubscribe.

Confirm dialog

Same pattern across adapters — declare, mount, open:

import {
  layerOptions,
  useLayer,
  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,
});

function RemoveButton() {
  const c = useLayer(confirm);
  return (
    <button
      onClick={() => void c.open({ title: "Remove item?" })}
    >
      Remove
    </button>
  );
}
import {
  layerOptions,
  useLayer,
} 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,
});

function RemoveButton() {
  const c = useLayer(confirm);
  return (
    <button
      onClick={() => void c.open({ title: "Remove item?" })}
    >
      Remove
    </button>
  );
}

Primitives

Stack hooks take an options bag plus optional trailing LayerClient. Drive hooks bind a layer definition and expose open() / dismiss(); observe hooks read matching states only.

Export Role
useStack({ stack?, select?, compare? }, client?) Subscribe to a stack slice via useSyncExternalStore
useQueuedStack({ stack?, select?, compare? }, client?) Queued snapshot slice
useLayer(options, client?) Drive — wired handle with open(), dismiss(), plus reactive state / queued / top
useLayerState({ key, stack?, select?, compare? }, client?) Observe — mounted layers matching key (LayerState[])
useLayerQueuedState({ key, stack?, select?, compare? }, client?) Observe — queued layers matching key

Ergonomic APIs

Export Role
useStackHandles(stack?, rootProps?) Headless { states, getCall } for custom renderers
StackSubscribe Render-prop subscription with selector isolation
useMutationFlow(call) Async-action helper — run(fn).orEnd(response)
useLayerGroup(call, options?) Child stack with open, dismissAll, states, Outlet, stackId
createStackHook(config?) Factory returning StackProvider, useAppStack, AppHost, AppLayer

Learn more

Last updated on September 4, 2026

Was this page helpful?