This post was written by a bot with my help, for a human who wants to use the idea. If you are an agent that was sent here by a project’s instructions, start at the demo repo’s AGENTS.md instead.
Here is a test of one feature’s logic. The feature is the orders screen of a small shipping dashboard; the flow is cancelling an order. Nothing is rendered and nothing is mocked.
test(
"cancelling a pending order",
Providers(
provideFakeClock,
provideFakeBackend,
provideAdmin,
provideOrders([{ customer: "Radia", status: "pending" }]),
provideFakeOrderService,
provideOrdersUx,
),
async ({ ordersUx, orders }) => {
await ordersUx.dispatch({ kind: "load" });
const [row] = ordersUx.getState().rows;
expect(row?.actions.find((a) => a.op.kind === "cancel")?.affordance.status).toBe("available");
await ordersUx.dispatch({ kind: "cancel", orderId: orders[0]!.id });
expect(ordersUx.getState().rows[0]?.status).toBe("cancelled");
},
);
Here is the same flow with the screen attached. This is a Storybook story from the demo repo, running in your browser right now. It boots the real React page on the same six providers as the test, and you can click Cancel yourself:
open in Storybook ↗ · story source
Look at what is absent. The test has no DOM: no React Testing Library, no vi.mock, no msw.
It reads the feature’s state off a plain object. The story has no backend: no server, no network
mocks, no fixture JSON. Both run on the same fake services, built by the same provider chain, and
every rule about what the user may do lives in one object that both of them reach.
The arrangement is three layers per feature. A Service is the port to the outside: an interface,
a real implementation, a fake, and one contract suite run against both. A Ux is a framework-free
object that owns its services, accepts commands, and emits a data-only state. A View is a React
component of { state, dispatch } that renders, dispatches, and decides nothing. The Ux gets
ordinary unit tests. The View gets stories. This is the humble view, Fowler’s name for Feathers’s
humble object applied to a screen, with a proof on each side of the seam. The stack is named for it:
the UI is the humble part.
The price is a naming convention, a four-line React adapter, and the discipline of keeping the View humble. There is no state-management library in it.
This post is for the TypeScript engineer with a React app where logic lives in hooks, data loading
happens in effects, and “testing the UI” means React Testing Library plus msw handlers copied
into every story. It builds on Providers and
Fixture Builders and reuses their provider chain and their
three-argument test() without re-explaining them. The pattern is the union of two production
codebases that each have half of it; the whole of it is the humble UI stack, a public demo at
PhotomancerArt/humble-ui-stack, with a
live dashboard and a
Storybook. The compiled code in this
post is a self-contained miniature of the demo’s orders feature. Like every example on this page, it
compiled and ran before the page was built; the repo is the full-scale reference.
Why you can’t write these today
The usual React feature has a shape. Data loads in an effect, the rules live in a hook, and the decisions happen in JSX:
function useOrders() {
const [orders, setOrders] = useState<Order[]>([]);
const { user } = useSession();
useEffect(() => {
void fetch("/api/orders")
.then((res) => res.json())
.then(setOrders);
}, []);
const act = async (id: string, verb: "cancel" | "refund") => {
await fetch(`/api/orders/${id}/${verb}`, { method: "POST" });
setOrders(await (await fetch("/api/orders")).json());
};
return {
orders,
cancel: (id: string) => act(id, "cancel"),
refund: (id: string) => act(id, "refund"),
isAdmin: user.role === "admin",
};
}
function OrdersTable() {
const { orders, cancel, refund, isAdmin } = useOrders();
return orders.map((order) => (
<tr key={order.id}>
<td>{order.customer}</td>
<td>{order.status}</td>
<td>
<button
disabled={order.status !== "pending"}
onClick={() => window.confirm("Cancel this order?") && cancel(order.id)}
>
Cancel
</button>
{isAdmin && order.status === "delivered" && (
<button onClick={() => refund(order.id)}>Refund</button>
)}
</td>
</tr>
));
}
It works, and it ships. Then:
- The rules are only reachable through the DOM. “Cancel only while pending” is a
disabledprop. To test it you render the table, find the button, and read an attribute. - Every story needs its own network. The hook fetches in an effect, so a story of
OrdersTableneedsmswhandlers for/api/orders, and each story’s data lives inside its handlers. - The same rule lives in several components.
isAdminis computed in the hook, combined with a status in the JSX, and checked again on the detail page. When the rule changes, you grep. - Nothing is reusable outside React. A CLI, an agent, or a second framework cannot cancel an order without the hook, and the hook cannot run without a component.
The cold open is the negative image. The rule lives in the Ux, once. The test reads it as data. The story renders it without a network. The same object would serve a CLI.
The Service layer
The orders feature reaches the outside through one interface. Refusals come back as values, so the Ux can explain them instead of catching them:
type Role = "admin" | "agent";
type Session = { role: Role };
type OrderStatus = "pending" | "shipped" | "delivered" | "cancelled" | "refunded";
interface Order {
id: string;
customer: string;
totalCents: number;
status: OrderStatus;
}
type BackendError = { code: "not_found" | "invalid_state" | "forbidden"; message: string };
type ServiceResult<T> = { ok: true; value: T } | { ok: false; error: BackendError };
/** The orders feature's port to the outside. Http talks to the backend; Fake is the backend. */
interface OrderService {
list(): Promise<Order[]>;
get(id: string): Promise<Order | undefined>;
cancel(id: string): Promise<ServiceResult<Order>>;
refund(id: string): Promise<ServiceResult<Order>>;
}
type OrderMethod = keyof OrderService;
interface AuthService {
current(): Promise<Session>;
}
There are two implementations. HttpOrderService calls the backend’s routes through a
fetch-shaped function; it is in the appendix. FakeOrderService is the backend without the
network. In the demo, packages/backend is the backend, simulated in TypeScript (domain,
in-memory store, rules, routes), and the fake calls it directly. What the fake adds is a script, so
a test or a story can set latency or make the next call fail:
function FakeOrderService({
backend,
script,
}: {
backend: Backend;
script: FakeScript<OrderMethod>;
}): OrderService {
const refused = (error: BackendError): ServiceResult<Order> => ({ ok: false, error });
return {
list: () => script.run("list", () => backend.orders.list()),
get: (id) => script.run("get", () => backend.orders.get(id)),
cancel: (id) => script.run("cancel", () => backend.orders.cancel(id), refused),
refund: (id) => script.run("refund", () => backend.orders.refund(id), refused),
};
}
/** The fake, and its script so the test or story can change the weather. */
function provideFakeOrderService({ backend, clock }: BackendCtx & { clock: Clock }) {
const ordersScript = FakeScript<OrderMethod>(clock);
return { orderService: FakeOrderService({ backend, script: ordersScript }), ordersScript };
}
The providers post covered provide* functions, so only the new part needs saying: the fake’s
provider hands its script back to the world as ordersScript, the way the recording logger in
that post handed back its messages.
A fake you trust is a fake you have tested. The contract suite is one function of a world, written once against the interface:
function describeOrderService(
name: string,
world: () => Promise<{ orderService: OrderService; backend: Backend }>,
) {
describe(`OrderService: ${name}`, () => {
test("cancel: pending → cancelled", world, async ({ orderService, backend }) => {
const order = await TestOrder(backend).create({ status: "pending" });
expect(await orderService.cancel(order.id)).toMatchObject({
ok: true,
value: { status: "cancelled" },
});
expect((await order.get()).status).toBe("cancelled");
});
test("cancel: shipped → invalid_state", world, async ({ orderService, backend }) => {
const order = await TestOrder(backend).create({ status: "shipped" });
expect(await orderService.cancel(order.id)).toMatchObject({
ok: false,
error: { code: "invalid_state" },
});
});
test("refund as agent → forbidden", world, async ({ orderService, backend }) => {
await backend.session.setRole("agent");
const order = await TestOrder(backend).create({ status: "delivered" });
expect(await orderService.refund(order.id)).toMatchObject({
ok: false,
error: { code: "forbidden" },
});
});
test("refund as admin after delivery → refunded", world, async ({ orderService, backend }) => {
await backend.session.setRole("admin");
const order = await TestOrder(backend).create({ status: "delivered" });
expect(await orderService.refund(order.id)).toMatchObject({
ok: true,
value: { status: "refunded" },
});
});
test("unknown id → not_found", world, async ({ orderService }) => {
expect(await orderService.cancel("ord-none")).toMatchObject({
ok: false,
error: { code: "not_found" },
});
});
});
}
It runs twice: once against the fake, and once against HttpOrderService over the backend’s
routes answering in-process, with no port:
describeOrderService(
"Fake",
Providers(provideFakeClock, provideFakeBackend, provideFakeOrderService),
);
describeOrderService(
"Http over in-process routes",
Providers(provideBackend, provideInProcessHttp, provideOrderService),
);
The fake is proven equivalent to the real thing on every rule the suite names, instead of assumed to be. When the backend grows a rule, the suite grows a case, and both implementations have to pass it. The demo runs the same kind of suite for its auth service and its shipment service.
The Ux layer
The Ux accepts ops. An op is a command as plain data, and it is the only thing a View is allowed to send:
type OrdersOp =
| { kind: "load" }
| { kind: "cancel"; orderId: string }
| { kind: "refund"; orderId: string }
| { kind: "dismissNotice" };
It emits state. State is the read model: serializable, complete, no functions. Each row carries its actions, and an action is an op paired with what the Ux has to say about it:
/**
* The semantic availability of an op. The Ux reports what is true: the op is available, it is
* disabled for a reason, the user is not allowed, or it does not apply. The surface decides how to
* present each one. No presentation words live here.
*/
type Affordance =
| { status: "available" }
| { status: "disabled"; reason: string; progress?: { label: string } }
| { status: "forbidden"; reason: string }
| { status: "unavailable" };
function available(): Affordance {
return { status: "available" };
}
function disabled(reason: string, progress?: { label: string }): Affordance {
return progress ? { status: "disabled", reason, progress } : { status: "disabled", reason };
}
function forbidden(reason: string): Affordance {
return { status: "forbidden", reason };
}
function unavailable(): Affordance {
return { status: "unavailable" };
}
/** An op the user may perform, with its affordance and label. `confirm` is presentation data. */
type Action<Op> = {
op: Op;
label: string;
affordance: Affordance;
destructive?: boolean;
confirm?: { title: string; body: string; confirmLabel: string };
};
type OrdersAction = Action<OrdersOp>;
type OrderRow = {
id: string;
customer: string;
total: string;
status: OrderStatus;
actions: OrdersAction[];
};
/** The read model the orders Ux emits: serializable, complete, no functions. */
type OrdersState = {
phase: "loading" | "ready" | "error";
rows: OrderRow[];
notice?: { tone: "error" | "info"; message: string };
};
The store underneath is twenty lines. The Ux interface is the store plus dispatch:
interface UxStore<S> {
getState: () => S;
subscribe: (listener: () => void) => () => void;
}
function UxStore<S>(initial: S): UxStore<S> & { setState: (next: S) => void } {
let state = initial;
const listeners = new Set<() => void>();
return {
getState: () => state,
subscribe: (listener) => {
listeners.add(listener);
return () => void listeners.delete(listener);
},
setState: (next) => {
if (Object.is(next, state)) return;
state = next;
for (const listener of [...listeners]) listener();
},
};
}
type DispatchResult =
| { ok: true }
| { ok: false; reason: "forbidden" | "disabled" | "unavailable" | "failed"; message: string };
/** A Ux owns its services, emits a data-only State, and accepts Ops. */
interface Ux<State, Op> extends UxStore<State> {
dispatch: (op: Op) => Promise<DispatchResult>;
}
And here is the orders Ux, in full. It takes its two services from the context, keeps the orders and
the role as private state, and projects them into OrdersState on every change:
type OrdersUxCtx = { orderService: OrderService; auth: AuthService };
/**
* The orders feature's logic, all of it: which ops apply to which order for whom, what happens
* while one is in flight, and how the backend's refusals come back as notices. Framework-free.
*/
function OrdersUx({ orderService, auth }: OrdersUxCtx): Ux<OrdersState, OrdersOp> {
const store = UxStore<OrdersState>({ phase: "loading", rows: [] });
let orders: Order[] = [];
let role: Role = "agent";
let phase: OrdersState["phase"] = "loading";
let notice: OrdersState["notice"];
const inFlight = new Map<string, "cancel" | "refund">();
function publish() {
const rows = orders.map(toRow);
store.setState(notice ? { phase, rows, notice } : { phase, rows });
}
function toRow(order: Order): OrderRow {
return {
id: order.id,
customer: order.customer,
total: usd.format(order.totalCents / 100),
status: order.status,
actions: [cancelAction(order), refundAction(order)],
};
}
function cancelAction(order: Order): OrdersAction {
const base = {
op: { kind: "cancel", orderId: order.id } as const,
label: "Cancel",
destructive: true,
confirm: {
title: `Cancel order ${order.id}?`,
body: `${order.customer} will be notified. This cannot be undone.`,
confirmLabel: "Cancel order",
},
};
if (inFlight.get(order.id) === "cancel") {
return { ...base, affordance: disabled("Cancelling", { label: "Cancelling…" }) };
}
if (order.status !== "pending") {
return { ...base, affordance: disabled("Only pending orders can be cancelled") };
}
return { ...base, affordance: available() };
}
function refundAction(order: Order): OrdersAction {
const base = { op: { kind: "refund", orderId: order.id } as const, label: "Refund" };
if (role !== "admin") return { ...base, affordance: forbidden("Admins only") };
if (inFlight.get(order.id) === "refund") {
return { ...base, affordance: disabled("Refunding", { label: "Refunding…" }) };
}
if (order.status !== "delivered" && order.status !== "cancelled") {
return { ...base, affordance: unavailable() };
}
return { ...base, affordance: available() };
}
async function load(): Promise<DispatchResult> {
phase = "loading";
publish();
try {
[orders, { role }] = await Promise.all([orderService.list(), auth.current()]);
phase = "ready";
publish();
return { ok: true };
} catch (error) {
phase = "error";
notice = { tone: "error", message: `Could not load orders: ${describeError(error)}` };
publish();
return { ok: false, reason: "failed", message: describeError(error) };
}
}
async function refetch(orderId: string) {
const fresh = await orderService.get(orderId);
orders = fresh
? orders.map((o) => (o.id === orderId ? fresh : o))
: orders.filter((o) => o.id !== orderId);
publish();
}
async function mutate(
op: Extract<OrdersOp, { kind: "cancel" | "refund" }>,
): Promise<DispatchResult> {
const row = store.getState().rows.find((r) => r.id === op.orderId);
const action = row?.actions.find((a) => a.op.kind === op.kind);
const affordance = action?.affordance ?? unavailable();
if (affordance.status !== "available") {
const message = "reason" in affordance ? affordance.reason : "Not applicable";
return { ok: false, reason: affordance.status, message };
}
inFlight.set(op.orderId, op.kind);
publish();
try {
const result = await (op.kind === "cancel"
? orderService.cancel(op.orderId)
: orderService.refund(op.orderId));
inFlight.delete(op.orderId);
if (result.ok) {
orders = orders.map((o) => (o.id === result.value.id ? result.value : o));
publish();
return { ok: true };
}
notice = { tone: "error", message: result.error.message };
await refetch(op.orderId);
return {
ok: false,
reason: result.error.code === "forbidden" ? "forbidden" : "failed",
message: result.error.message,
};
} catch (error) {
inFlight.delete(op.orderId);
notice = { tone: "error", message: describeError(error) };
publish();
return { ok: false, reason: "failed", message: describeError(error) };
}
}
return {
getState: store.getState,
subscribe: store.subscribe,
dispatch: (op) => {
switch (op.kind) {
case "load":
return load();
case "cancel":
case "refund":
return mutate(op);
case "dismissNotice":
notice = undefined;
publish();
return Promise.resolve({ ok: true });
}
},
};
}
type OrdersUx = ReturnType<typeof OrdersUx>;
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
function describeError(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
/** The last link of an orders world. */
function provideOrdersUx(ctx: OrdersUxCtx) {
return { ordersUx: OrdersUx(ctx) };
}
Read cancelAction and refundAction again. Every rule about what the user may do to an order
is in those two functions, once. Then read mutate: on dispatch, the Ux re-reads the affordance
it last published and refuses without calling the service if it is not available, so a stale View
cannot make it do what the state said it could not. When the backend refuses anyway, the refusal
becomes a notice and the row is refetched.
The demo’s version has one more thing, an event-bus subscription, so that a shipment delivered in
the other feature refreshes its order here and a role switch recomputes the affordances. It is the
same file with a dispose.
Now the tests. They have the cold open’s shape, and they need no DOM because the state is data:
const asAgent = Providers(
provideFakeClock,
provideFakeBackend,
provideAgent,
provideOrders([
{ customer: "Radia", status: "pending" },
{ customer: "Ada", status: "shipped" },
{ customer: "Grace", status: "delivered" },
]),
provideFakeOrderService,
provideOrdersUx,
);
const asAdmin = Providers(
provideFakeClock,
provideFakeBackend,
provideAdmin,
provideOrders([
{ customer: "Radia", status: "pending" },
{ customer: "Ada", status: "shipped" },
{ customer: "Grace", status: "delivered" },
{ customer: "Lin", status: "cancelled" },
]),
provideFakeOrderService,
provideOrdersUx,
);
function affordance(ctx: { ordersUx: OrdersUx }, row: number, kind: OrdersOp["kind"]) {
return ctx.ordersUx.getState().rows[row]?.actions.find((a) => a.op.kind === kind)?.affordance;
}
test("cancel is available only while pending", asAgent, async (ctx) => {
await ctx.ordersUx.dispatch({ kind: "load" });
expect(affordance(ctx, 0, "cancel")).toEqual({ status: "available" });
expect(affordance(ctx, 1, "cancel")).toEqual({
status: "disabled",
reason: "Only pending orders can be cancelled",
});
});
test("refund is forbidden for agents, with the reason", asAgent, async (ctx) => {
await ctx.ordersUx.dispatch({ kind: "load" });
expect(affordance(ctx, 2, "refund")).toEqual({ status: "forbidden", reason: "Admins only" });
});
test("refund is available to admins after delivery or cancellation", asAdmin, async (ctx) => {
await ctx.ordersUx.dispatch({ kind: "load" });
expect(affordance(ctx, 0, "refund")).toEqual({ status: "unavailable" });
expect(affordance(ctx, 1, "refund")).toEqual({ status: "unavailable" });
expect(affordance(ctx, 2, "refund")).toEqual({ status: "available" });
expect(affordance(ctx, 3, "refund")).toEqual({ status: "available" });
});
test(
"dispatch re-validates: a disabled op is refused without calling the service",
asAgent,
async ({ ordersUx, orders, backend }) => {
await ordersUx.dispatch({ kind: "load" });
const result = await ordersUx.dispatch({ kind: "cancel", orderId: orders[1]!.id });
expect(result).toEqual({
ok: false,
reason: "disabled",
message: "Only pending orders can be cancelled",
});
expect((await backend.orders.get(orders[1]!.id))?.status).toBe("shipped");
},
);
test(
"while an op is in flight its action is disabled with progress",
asAgent,
async ({ ordersUx, orders, ordersScript, clock }) => {
await ordersUx.dispatch({ kind: "load" });
ordersScript.latencyMs = 100;
const cancelling = ordersUx.dispatch({ kind: "cancel", orderId: orders[0]!.id });
await clock.settle();
expect(ordersUx.getState().rows[0]?.actions[0]?.affordance).toEqual({
status: "disabled",
reason: "Cancelling",
progress: { label: "Cancelling…" },
});
await clock.advance(100);
expect(await cancelling).toEqual({ ok: true });
expect(ordersUx.getState().rows[0]?.status).toBe("cancelled");
},
);
test(
"the backend's refusal becomes a notice and the row is refetched",
asAdmin,
async ({ ordersUx, orders, backend }) => {
await ordersUx.dispatch({ kind: "load" });
// The UI still shows refund as available; the server no longer agrees.
await backend.session.setRole("agent");
const result = await ordersUx.dispatch({ kind: "refund", orderId: orders[2]!.id });
expect(result).toMatchObject({ ok: false, reason: "forbidden" });
expect(ordersUx.getState().notice).toEqual({
tone: "error",
message: "Only admins can refund orders",
});
expect(ordersUx.getState().rows[2]?.status).toBe("delivered");
await ordersUx.dispatch({ kind: "dismissNotice" });
expect(ordersUx.getState().notice).toBeUndefined();
},
);
test(
"a scripted service failure becomes a notice",
asAgent,
async ({ ordersUx, orders, ordersScript }) => {
await ordersUx.dispatch({ kind: "load" });
ordersScript.failNext("cancel", {
code: "invalid_state",
message: "Order changed on the server",
});
const result = await ordersUx.dispatch({ kind: "cancel", orderId: orders[0]!.id });
expect(result).toEqual({ ok: false, reason: "failed", message: "Order changed on the server" });
expect(ordersUx.getState().notice?.message).toBe("Order changed on the server");
expect(ordersUx.getState().rows[0]?.status).toBe("pending");
},
);
test("state is data", asAdmin, async ({ ordersUx }) => {
await ordersUx.dispatch({ kind: "load" });
expect(JSON.parse(JSON.stringify(ordersUx.getState()))).toEqual(ordersUx.getState());
expect(ordersUx.getState().rows[0]?.total).toBe("$42.00");
});
Two worlds, a three-line helper, and each rule gets a test the size of the rule. The in-flight test holds the fake open with a scripted latency and a fake clock. The failure test scripts the fake’s next call. The refusal test switches the backend’s role after the Ux has loaded. None of them render anything, and an agent can run the whole file in a loop without a browser.
Actions are data
An affordance has four states, and the split between them is the part of this pattern I care most about.
available means the op will go through. disabled means it applies but cannot run now, with a
reason; an in-flight op is a disabled one with a progress label. forbidden means this user may
not, with a reason. unavailable means the op does not apply to this row. The Ux reports which
one is true. The surface decides how each looks: a refund the user may not perform is a locked
button in Dispatch, it could be hidden in another product, and the Ux would not change.
Confirmation is presentation too. The cancel action carries confirm; the button asks and
dispatches on yes; the Ux never models a pending confirmation.
One rule keeps this honest: no action field exists without a renderer and a story that honors it.
Both production codebases behind this post grew action metadata that nothing rendered: an enablement
enum here, a short label there, a hidden predicate no surface read. A field no renderer consumes is
a lie the type system will not catch. So the demo has one renderer, ActionButton in ui-app,
and its All States story is the proof:
/**
* An Action, rendered. This is where the default surface decides how each affordance looks:
*
* - available: an enabled button (destructive variant when the action says so).
* - disabled: a disabled button; the reason on hover and in aria-description; in-flight
* progress replaces the label.
* - forbidden: a disabled button with a lock and the reason. The user learns what they lack.
* - unavailable: nothing. The op does not apply, so the surface omits it.
*
* confirm is presentation: the button opens a ConfirmDialog and dispatches on confirm.
*/
export function ActionButton<Op>(props: { action: Action<Op>; onDispatch: (op: Op) => void }) {
const { action } = props;
const { affordance } = action;
const [confirming, setConfirming] = useState(false);
if (affordance.status === "unavailable") return null;
const dispatch = () => props.onDispatch(action.op);
if (affordance.status === "available") {
return (
<>
<Button
variant={action.destructive ? "destructive" : "outline"}
data-affordance="available"
onClick={action.confirm ? () => setConfirming(true) : dispatch}
>
{action.label}
</Button>
{action.confirm && (
<ConfirmDialog open={confirming} onOpenChange={setConfirming} {...action.confirm} onConfirm={dispatch} />
)}
</>
);
}
const progress = affordance.status === "disabled" ? affordance.progress : undefined;
return (
<Tooltip content={affordance.reason}>
<Button disabled aria-description={affordance.reason} data-affordance={affordance.status}>
{affordance.status === "forbidden" && <LockIcon aria-hidden />}
{progress ? <InlineProgress label={progress.label} /> : action.label}
</Button>
</Tooltip>
);
}
One more check happens before anything runs. A world that cannot build the Ux does not compile, so a test or a story with a missing service fails at the type level:
// @ts-expect-error A world without an order service cannot build the orders Ux.
Providers(provideFakeClock, provideFakeBackend, provideOrdersUx);
The View layer
React meets the Ux in one hook. This is the entire adapter:
export function useUx<S, Op>(ux: Ux<S, Op>) {
const state = useSyncExternalStore(ux.subscribe, ux.getState, ux.getState);
return { state, dispatch: ux.dispatch };
}
The View is a function of { state, dispatch }. It has no import from service/, none from the
auth service, and no if about the domain. Its one lookup table maps a status to a color, which
is presentation:
const tone: Record<OrderStatus, StatusTone> = {
pending: "neutral",
shipped: "info",
delivered: "success",
cancelled: "warning",
refunded: "danger",
};
/** Renders OrdersState, dispatches OrdersOps, decides nothing. */
export function OrdersView({ state, dispatch }: { state: OrdersState; dispatch: (op: OrdersOp) => void }) {
const columns: Column<OrderRow>[] = [
{ key: "id", header: "Order", cell: (row) => <span className="font-mono">{row.id}</span> },
{ key: "customer", header: "Customer", cell: (row) => row.customer },
{ key: "total", header: "Total", cell: (row) => row.total },
{
key: "status",
header: "Status",
cell: (row) => <StatusBadge tone={tone[row.status]}>{row.status}</StatusBadge>,
},
{
key: "actions",
header: "",
cell: (row) => <ActionBar actions={row.actions} onDispatch={dispatch} />,
},
];
return (
<ListLayout title="Orders" description="Cancel while pending; refund once delivered.">
{state.notice && (
<Notice tone={state.notice.tone} onDismiss={() => dispatch({ kind: "dismissNotice" })}>
{state.notice.message}
</Notice>
)}
{state.phase === "loading" && state.rows.length === 0 ? (
<InlineProgress label="Loading orders…" />
) : (
<DataTable rows={state.rows} columns={columns} rowKey={(row) => row.id} />
)}
</ListLayout>
);
}
ActionBar is a row of ActionButtons. The View does not know what an affordance means; it
hands each row’s actions to the one component that does. The Page is the one place React and the Ux
touch:
export function OrdersPage({ ordersUx }: { ordersUx: OrdersUx }) {
const { state, dispatch } = useUx(ordersUx);
useEffect(() => {
void dispatch({ kind: "load" });
}, [dispatch]);
return <OrdersView state={state} dispatch={dispatch} />;
}
Because the View takes state as a prop, its stories are hand-built states, and every state of the screen is one click away without a backend:
const meta = {
title: "orders/OrdersView",
component: OrdersView,
args: { dispatch: fn(), state: sampleStates.readyAsAdmin },
} satisfies Meta<typeof OrdersView>;
export const Loading: Story = { args: { state: sampleStates.loading } };
export const ReadyAsAgent: Story = { args: { state: sampleStates.readyAsAgent } };
export const InFlight: Story = { args: { state: sampleStates.inFlight } };
export const LoadFailed: Story = { args: { state: sampleStates.loadFailed } };
The demo’s sampleStates has seven of these, including the ones that are hard to reach by
clicking: a cancel in flight, a load that failed.
Page stories
The component stories prove the View renders every state. The page stories prove the whole feature works on the screen, and they start from the test’s world:
// The same chain the cold-open Ux test uses; only the last link renders.
const asAdmin = Providers(
provideFakeClock,
provideFakeBackend(),
provideAdmin,
provideOrders(demoOrders),
provideFakeOrderService,
provideOrdersUx,
);
function Booted({ world }: { world: typeof asAdmin }) {
return (
<World provider={world} fallback={<InlineProgress label="Booting world…" />}>
{({ ordersUx }) => <OrdersPage ordersUx={ordersUx} />}
</World>
);
}
const meta = {
title: "orders/OrdersPage",
component: Booted,
args: { world: asAdmin },
} satisfies Meta<typeof Booted>;
export const AsAdmin: Story = { name: "As admin" };
export const TestCancelPendingOrder: Story = {
name: "Test: cancel a pending order",
play: async ({ canvas, userEvent }) => {
await expect(await canvas.findByTestId("orders-status-ord-1001")).toHaveTextContent("pending");
await userEvent.click(await canvas.findByTestId("orders-cancel-ord-1001"));
await userEvent.click(await screen.findByTestId("orders-cancel-ord-1001-confirm-confirm"));
await waitFor(() =>
expect(canvas.getByTestId("orders-status-ord-1001")).toHaveTextContent("cancelled"),
);
},
};
World runs a provider chain once and renders its children with the context. It is the story-side
twin of test(name, world, fn). The play test then clicks through the flow the cold-open test
dispatched, and CI runs it in a real browser. Here it is, running:
open in Storybook ↗ · story source
Scroll back to the top. Every line of the test is now code you have read, and the story is the same six providers with a page rendered over the last one. Two proofs, one set of fakes: the Ux test says the rules are right, the page story says the screen shows them, and neither needed a mock.
Features and boundaries
Dispatch has two features, orders and shipments, and they never import each other. A delivered
shipment changes its order’s status; that rule runs in the backend, feat-shipments publishes
shipment.delivered on a shared bus, and feat-orders refetches the order. The packages are
the boundaries, so a violation is a build failure rather than a review comment:
packages/
ux-core/ the pattern: Providers, test(), UxStore, Ux, Affordance, Action, EventBus, Clock;
ux-core/react: useUx, World
ui-design/ Tailwind theme and tokens
ui-base/ shadcn/ui primitives; never imports ux-core
ui-app/ the app's common language: DataTable, StatusBadge, ActionButton, ActionBar, Notice
backend/ the simulated backend: domain, store, rules, routes; testing/: builders and worlds
app-core/ AuthService, HttpClient, DispatchEvent; testing/: provideFakeBackend
feat-orders/ service/ ux/ view/ testing/
feat-shipments/ same shape
apps/
api/ node server mounting the routes
dashboard/ Vite + React; two composition roots: over the network, or routes in the browser
storybook/ every package's stories, plus the full-app story
Arrows mean “may import”:
ui-design ← ui-base ← ui-app ← feat-* → app-core → ux-core
↑ ux-core ↑ backend (fakes, routes, worlds)
apps → feat-*, ui-app, app-core, backend, ux-core
Each package’s tests run alone. The deployed dashboard is the real app over the real HTTP services, with the backend’s routes mounted in the browser, so GitHub Pages serves it with no server. The full-app story does the same inside Storybook:
open the dashboard ↗ · open in Storybook ↗
The humble view is one of the stack’s nine decisions. The others, providers and worlds, builders, component layers, feature modules, are in the humble UI stack’s README, each in the same shape: the problem, the decision, what it replaces, where the idea comes from, and what it costs. The layering itself is ADR 0001.
Provenance
Neither production codebase behind this post has the whole pattern. Each has a different half, and the demo is the union.
LightPlayer, a Rust and Dioxus desktop app of mine, has the Ux layer, named and tested. Its studio surfaces own the device link and expose UI-independent view DTOs and typed actions; 30 end-to-end tests drive the real effects layer over a scripted fake device, in a crate with about 1150 tests. Its stories, though, render hand-built DTO fixtures. No story boots a Ux on fakes, so the screen and the logic are proven separately and nothing proves they agree. That is the gap this demo’s page stories close, and a to-do for LightPlayer.
The SaaS monorepo behind the earlier posts has the other half. Its full-page stories boot the app on a memory database through a provider chain, run the real server load inside the real context, and render the real page; 49 of its 439 stories work this way, and its query layer has memory and Postgres implementations that pass the same tests. It has no Ux layer. Page logic lives in load functions and components, and its action type is a presentation DTO with callbacks. Adding a Ux there is a refactor, because the worlds already exist.
The naming came from both. LightPlayer’s split between a typed controller op and a UI action held
up; the monorepo’s single action type with callbacks did not. The demo keeps Op and Action
as separate words for that reason.
For agents and teams
I built the demo instead of writing the pattern up from memory because I wanted one place to point at, for people and for coding agents, that says where each kind of code goes. The repo’s AGENTS.md is that place: a table of where code goes, the naming vocabulary, the dependency rule, and the rules as a checklist:
- The View decides nothing. No domain
ifin JSX; if a View needs a fact, the Ux puts it in State. - State is data. It must survive
JSON.parse(JSON.stringify(s)). - Every rule lives in the Ux, once, with a test.
- Re-validate on dispatch.
- No action field without a renderer and a story that honors it.
- Confirmation is presentation; progress is data.
- Authorization twice: the Ux computes
forbiddenso the UI can explain, the backend enforces. - Every service ships a fake, a real implementation, and one contract suite run against both.
- Ux tests and page stories share worlds.
To add a feature, copy feat-orders, rename by the table, and write the contract suite first. An
agent with that file can build a feature in a test loop without opening a browser, then prove the
screen in a story. That is the payoff past the tests: the view is humble so that everything else can
be tested.
Appendix: the world
Everything the examples depend on, in full. First the simulated backend. It owns the rules, the way
packages/backend does in the demo, and both implementations of OrderService end up here:
interface Backend {
orders: {
list(): Promise<Order[]>;
get(id: string): Promise<Order | undefined>;
create(input: Omit<Order, "id">): Promise<Order>;
cancel(id: string): Promise<ServiceResult<Order>>;
refund(id: string): Promise<ServiceResult<Order>>;
};
session: {
get(): Promise<Session>;
setRole(role: Role): Promise<void>;
};
}
function Backend(): Backend {
const rows = new Map<string, Order>();
let session: Session = { role: "agent" };
const fail = (code: BackendError["code"], message: string): ServiceResult<Order> => ({
ok: false,
error: { code, message },
});
const save = (order: Order): ServiceResult<Order> => {
rows.set(order.id, order);
return { ok: true, value: order };
};
return {
orders: {
list: async () => [...rows.values()],
get: async (id) => rows.get(id),
create: async (input) => {
const order = { id: `ord-${1000 + rows.size + 1}`, ...input };
rows.set(order.id, order);
return order;
},
cancel: async (id) => {
const order = rows.get(id);
if (!order) return fail("not_found", `No such order: ${id}`);
if (order.status !== "pending") {
return fail(
"invalid_state",
`Order ${id} is ${order.status}; only pending orders can be cancelled`,
);
}
return save({ ...order, status: "cancelled" });
},
refund: async (id) => {
const order = rows.get(id);
if (!order) return fail("not_found", `No such order: ${id}`);
if (session.role !== "admin") return fail("forbidden", "Only admins can refund orders");
if (order.status !== "delivered" && order.status !== "cancelled") {
return fail(
"invalid_state",
`Order ${id} is ${order.status}; only delivered or cancelled orders can be refunded`,
);
}
return save({ ...order, status: "refunded" });
},
},
session: {
get: async () => session,
setRole: async (role) => {
session = { role };
},
},
};
}
function provideBackend() {
return { backend: Backend() };
}
type BackendCtx = ReturnType<typeof provideBackend>;
/** The fake world's foundation: the simulated backend and a fake auth service over its session. */
function provideFakeBackend() {
const { backend } = provideBackend();
const auth: AuthService = { current: () => backend.session.get() };
return { backend, auth };
}
The real implementation, and the routes it talks to. backendRoutes returns a fetch-shaped
function; the demo’s version is a Hono app, and the api server, the browser build, and the contract
test all mount the same one:
type Fetch = (path: string, init?: { method?: string }) => Promise<Response>;
function HttpOrderService({ http }: { http: Fetch }): OrderService {
return {
list: () => readJson<Order[]>(http("/orders")),
get: async (id) => {
const res = await http(`/orders/${id}`);
return res.status === 404 ? undefined : readJson<Order>(Promise.resolve(res));
},
cancel: (id) => readResult<Order>(http(`/orders/${id}/cancel`, { method: "POST" })),
refund: (id) => readResult<Order>(http(`/orders/${id}/refund`, { method: "POST" })),
};
}
async function readJson<T>(response: Promise<Response>): Promise<T> {
const res = await response;
if (!res.ok) throw new Error(`${res.status} from the backend`);
return (await res.json()) as T;
}
async function readResult<T>(response: Promise<Response>): Promise<ServiceResult<T>> {
const res = await response;
return res.ok
? { ok: true, value: (await res.json()) as T }
: { ok: false, error: (await res.json()) as BackendError };
}
function backendRoutes(backend: Backend): Fetch {
const json = (body: unknown, status = 200) => Response.json(body, { status });
const statusOf: Record<BackendError["code"], number> = {
not_found: 404,
invalid_state: 409,
forbidden: 403,
};
return async (path, init) => {
const method = init?.method ?? "GET";
const [, resource, id, verb] = path.split("/");
if (resource !== "orders") return json({ message: "no route" }, 404);
if (method === "GET" && id === undefined) return json(await backend.orders.list());
if (method === "GET" && id !== undefined && verb === undefined) {
const order = await backend.orders.get(id);
return order
? json(order)
: json({ code: "not_found", message: `No such order: ${id}` }, 404);
}
if (method === "POST" && id !== undefined && (verb === "cancel" || verb === "refund")) {
const result = await backend.orders[verb](id);
return result.ok ? json(result.value) : json(result.error, statusOf[result.error.code]);
}
return json({ message: "no route" }, 404);
};
}
function provideInProcessHttp({ backend }: BackendCtx) {
return { http: backendRoutes(backend) };
}
function provideOrderService({ http }: { http: Fetch }) {
return { orderService: HttpOrderService({ http }) };
}
The fake’s script and the fake clock. The script waits on the clock, so a test can hold a call open and watch the state while it is in flight:
interface Clock {
sleep(ms: number): Promise<void>;
}
/** What a fake service does besides call the backend: wait, and fail on cue. */
interface FakeScript<Method extends string> {
latencyMs: number;
failNext(method: Method, error: BackendError): void;
run<T>(method: Method, call: () => Promise<T>, onError?: (error: BackendError) => T): Promise<T>;
}
function FakeScript<Method extends string>(clock: Clock): FakeScript<Method> {
let scripted: { method: Method; error: BackendError } | undefined;
const script: FakeScript<Method> = {
latencyMs: 0,
failNext: (method, error) => {
scripted = { method, error };
},
run: async (method, call, onError) => {
if (script.latencyMs > 0) await clock.sleep(script.latencyMs);
if (scripted?.method === method) {
const { error } = scripted;
scripted = undefined;
if (onError) return onError(error);
throw new Error(error.message);
}
return call();
},
};
return script;
}
function provideFakeClock() {
let now = 0;
const timers: Array<{ at: number; fire: () => void }> = [];
const flush = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
const clock = {
sleep: (ms: number) => new Promise<void>((fire) => void timers.push({ at: now + ms, fire })),
/** Lets every pending promise chain run without moving time. */
settle: flush,
/** Moves time forward and fires every timer that comes due. */
advance: async (ms: number) => {
now += ms;
for (const timer of timers.filter((t) => t.at <= now)) {
timers.splice(timers.indexOf(timer), 1);
timer.fire();
await flush();
}
},
};
return { clock };
}
The builder and the world links, from the fixture-builders post. TestOrder creates through the
backend’s own create path and returns a live handle; provideOrders composes it as a provider:
const testStrCounters = new Map<string, number>();
function testStr(prefix: string): string {
const count = testStrCounters.get(prefix) ?? 0;
testStrCounters.set(prefix, count + 1);
return `${prefix}-${count}`;
}
type OrderSpec = Partial<Omit<Order, "id">>;
type OrderHandle = { id: string; get: () => Promise<Order> };
function TestOrder(backend: Backend) {
const byId = (id: string): OrderHandle => ({
id,
get: async () => {
const order = await backend.orders.get(id);
if (!order) throw new Error(`Order ${id} no longer exists`);
return order;
},
});
return Object.assign(byId, {
create: async (overrides: OrderSpec = {}): Promise<OrderHandle> => {
const order = await backend.orders.create({
customer: testStr("customer"),
totalCents: 4_200,
status: "pending",
...overrides,
});
return byId(order.id);
},
});
}
/** Creates orders through the builder and hands their handles to the world as `orders`. */
function provideOrders(specs: OrderSpec[]) {
return async ({ backend }: BackendCtx) => {
const orders: OrderHandle[] = [];
for (const spec of specs) orders.push(await TestOrder(backend).create(spec));
return { orders };
};
}
async function provideAdmin({ backend }: BackendCtx) {
await backend.session.setRole("admin");
return {};
}
async function provideAgent({ backend }: BackendCtx) {
await backend.session.setRole("agent");
return {};
}
Finally, the machinery from the earlier posts: Providers with enough overloads for a six-link
world, and the test() that takes one. The demo vendors the same code from
ts-provide without its ambient context, because
AsyncLocalStorage is a server-side trick: in the browser a Ux takes its context explicitly,
which is also why the tests read the way they do.
import { test as vitestTest } from "vitest";
type Ctx = object;
type MaybePromise<T> = T | Promise<T>;
export function Providers<A extends Ctx>(a: () => MaybePromise<A>): () => Promise<A>;
export function Providers<A extends Ctx, B extends Ctx>(
a: () => MaybePromise<A>,
b: (ctx: A) => MaybePromise<B>,
): () => Promise<A & B>;
export function Providers<A extends Ctx, B extends Ctx, C extends Ctx>(
a: () => MaybePromise<A>,
b: (ctx: A) => MaybePromise<B>,
c: (ctx: A & B) => MaybePromise<C>,
): () => Promise<A & B & C>;
export function Providers<A extends Ctx, B extends Ctx, C extends Ctx, D extends Ctx>(
a: () => MaybePromise<A>,
b: (ctx: A) => MaybePromise<B>,
c: (ctx: A & B) => MaybePromise<C>,
d: (ctx: A & B & C) => MaybePromise<D>,
): () => Promise<A & B & C & D>;
export function Providers<
A extends Ctx,
B extends Ctx,
C extends Ctx,
D extends Ctx,
E extends Ctx,
>(
a: () => MaybePromise<A>,
b: (ctx: A) => MaybePromise<B>,
c: (ctx: A & B) => MaybePromise<C>,
d: (ctx: A & B & C) => MaybePromise<D>,
e: (ctx: A & B & C & D) => MaybePromise<E>,
): () => Promise<A & B & C & D & E>;
export function Providers<
A extends Ctx,
B extends Ctx,
C extends Ctx,
D extends Ctx,
E extends Ctx,
F extends Ctx,
>(
a: () => MaybePromise<A>,
b: (ctx: A) => MaybePromise<B>,
c: (ctx: A & B) => MaybePromise<C>,
d: (ctx: A & B & C) => MaybePromise<D>,
e: (ctx: A & B & C & D) => MaybePromise<E>,
f: (ctx: A & B & C & D & E) => MaybePromise<F>,
): () => Promise<A & B & C & D & E & F>;
export function Providers(
...providers: Array<(ctx: object) => MaybePromise<object>>
): () => Promise<object> {
return async () => {
let context: object = {};
for (const provider of providers) {
context = { ...context, ...(await provider(context)) };
}
return context;
};
}
type AnyProvider = () => MaybePromise<object>;
export function test(name: string, fn: () => void | Promise<void>): void;
export function test<TProvider extends AnyProvider>(
name: string,
provider: TProvider,
fn: (ctx: Awaited<ReturnType<TProvider>>) => void | Promise<void>,
): void;
export function test(
name: string,
providerOrFn: AnyProvider | (() => void | Promise<void>),
fn?: (ctx: object) => void | Promise<void>,
): void {
if (fn === undefined) {
vitestTest(name, providerOrFn as () => void | Promise<void>);
return;
}
vitestTest(name, async () => {
await fn(await (providerOrFn as AnyProvider)());
});
}