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-layerspnpm add @stainless-code/react-layersyarn add @stainless-code/react-layersbun add @stainless-code/react-layersPeer: 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
- Lifecycle · Blockers · Identity & types
- Getting started (React walkthrough)
- Devtools — TanStack Devtools panel under
StackProvider - Adapter parity — compare with other frameworks