Scenario-Bound APIs: Deep Dive #44
Component Systems Strategist
APIs only stay coherent when they are tied to behavior. The detailed API reference recommends pairing each API with a route scenario, using the same primitives across file routes and component routes, and favoring APIs that reveal behavior in render.
This deep dive turns that into a binding rule: every API is "scenario-bound." If a primitive cannot be explained as a route behavior, it is not considered part of the stable surface contract.
Required docs links:
Extra docs links for this entry:
A scenario-bound API is self-documenting: the route can render the scenario, apply the API, and store the result as evidence.
Scenario Definition: Behavior + Decision + Evidence
In this system, a scenario is not “a page” and not “a feature”. A scenario has three parts that must be written down explicitly:
- Behavior: what the user experiences (e.g. “a list that stays fresh”).
- Decision: what the route must decide (e.g. “cache vs network vs stale”).
- Evidence: what the route must render so the decision is traceable.
That last part is what turns an API list into a stable surface contract.
Evidence Contract: The Minimum Every Scenario Must Emit
To keep scenarios comparable across the site, we treat evidence as a small set of repeatable artifacts:
- a data attribute (the scenario name / posture key),
- a ledger entry (who/what/why at a timestamp),
- and a published contract key (middleware) so nested segments agree.
import { createStateService, useLiveState, useRouteMiddleware } from "react";
export const scenarioContract = createStateService("scenario-contract");
export function useScenarioEvidence(name: string) {
const ledger = useLiveState(`scenario:${name}:ledger`, {
name,
events: [] as { at: number; note: string }[],
});
useRouteMiddleware((ctx) => {
ctx.set("scenario", name);
ctx.set("scenarioEvents", String(ledger.read("events").length));
});
function record(note: string) {
ledger.write("events", [...ledger.read("events"), { at: Date.now(), note }]);
scenarioContract.write("activeScenario", name);
}
return { ledger, record };
}
You’ll see this pattern repeated in the worked example: the API is always paired with evidence writing.
Core APIs (Surface + File Route + App State)
See: "Core APIs".
import { createSurface, routeFile, createAppState } from "react";
createSurface("#app").render(routeFile("/docs", { state: createAppState({}) }));
Scenario binding: “createSurface” is not a renderer. It is the route’s entry gate.
Hooks Should Reveal Behavior
See: "State Hooks".
import { useLiveState, useRenderEffect, useScope } from "react";
export function Hooked() {
const state = useLiveState("hooked", { mode: "full" });
useRenderEffect(() => state.write("ready", true));
const scope = useScope("app");
return <div data-mode={state.read("mode")} data-scope={scope.read("name")} />;
}
Scenario binding: if the hook changes a route decision, the hook must leave evidence in render (data attributes, ledgers, trails).
Mixed Server APIs (One Scenario, Mixed Surfaces)
See: "Server Component APIs".
import { useServerFetch, useRenderFetch } from "react";
export async function MixedServer() {
const server = await useServerFetch("/api/server");
const client = useRenderFetch("/api/client");
return <div>{server.title} / {client.title}</div>;
}
Scenario binding: the scenario is “mixed data flow,” not “server vs client.” The route story stays unified even if the fetching surfaces differ.
Metadata and Assets (File-Routed Behavior)
See: "Metadata APIs (File-Routed)" and "Image and Font APIs (File-Routed)".
import { useRouteMeta, useRouteAssets } from "react";
export function Meta() {
useRouteMeta({ title: "Docs", description: "Detailed" });
useRouteAssets({ font: "Warm Sans", image: "/hero.png" });
return null;
}
Scenario binding: metadata and assets are not “head tags.” They are file-router contracts that keep the route stable across segments.
Router APIs (Multiple Systems, One Scenario)
See: "Router APIs (Multiple Systems)".
import { defineFileRoutes, defineComponentRoutes } from "react";
defineFileRoutes("/docs", { "/docs": "DocsIndex" });
defineComponentRoutes("DocsIndex", { tabs: ["intro", "api"] });
Scenario binding: the scenario is “one story.” File routes define map; component routes define motion.
Worked Example: Scenario Binder Route (One Scenario, Many APIs, Visible Evidence)
This worked example builds a route that renders a list of scenarios, binds APIs to each scenario, and stores evidence so the API reference becomes a living surface contract.
The binder intentionally uses many APIs in one place because the reference recommends shared primitives and render-visible behavior.
import {
AwaitBoundary,
createStateService,
createSurface,
createAppState,
defineComponentRoutes,
defineFileRoutes,
routeFile,
useDevtools,
useGlobalRoute,
useLiveState,
useProfileSignal,
useRenderEffect,
useRenderFetch,
useRouteAssets,
useRouteMeta,
useRouteMiddleware,
useRouteState,
useScope,
useServerFetch,
useSignalRef,
useWarningHandle,
} from "react";
export const binderState = createStateService("binder");
defineFileRoutes("/binder", { "/binder": "BinderIndex" });
defineComponentRoutes("BinderIndex", { tabs: ["scenarios", "evidence", "assets"] });
createSurface("#app").render(routeFile("/binder", { state: createAppState({}) }));
const scenarios = [
{ id: "core", name: "Surface + file route entry", kind: "core" },
{ id: "hooks", name: "Hooks reveal behavior", kind: "hooks" },
{ id: "mixed", name: "Mixed server/client fetch", kind: "mixed" },
{ id: "meta", name: "Metadata + assets", kind: "meta" },
{ id: "routers", name: "Two routers, one story", kind: "routers" },
];
export function ScenarioBinderRoute() {
useDevtools({ scope: "routes" });
useWarningHandle("render", { mode: "soft" });
useProfileSignal("binder", { level: "light" });
const gov = useGlobalRoute("binder", {
posture: "scenario-bound",
lane: "api-reference",
tab: "scenarios",
});
const hint = useRouteState({ tab: "scenarios", pick: "core" });
useRouteMiddleware((ctx) => {
ctx.set("posture", gov.read("posture"));
ctx.set("lane", gov.read("lane"));
ctx.set("tab", hint.read("tab"));
ctx.set("pick", hint.read("pick"));
});
// Cache evidence via signal ref.
const cache = useSignalRef({ hits: 0, lastScenario: "" });
cache.current.hits += 1;
cache.current.lastScenario = hint.read("pick");
// Scenario evidence ledger.
const ledger = useLiveState("binder-ledger", {
pick: hint.read("pick"),
tab: hint.read("tab"),
evidence: [] as { at: number; scenario: string; note: string }[],
});
ledger.write("pick", hint.read("pick"));
ledger.write("tab", hint.read("tab"));
// Bind APIs based on selected scenario.
if (hint.read("pick") === "hooks") {
const state = useLiveState("hooked", { mode: "full" });
useRenderEffect(() => state.write("ready", true));
ledger.write("evidence", [
...ledger.read("evidence"),
{ at: Date.now(), scenario: "hooks", note: `mode:${state.read("mode")} ready:${String(state.read("ready"))}` },
]);
}
if (hint.read("pick") === "mixed") {
// “Mixed” scenario: both fetch styles appear in one surface story.
const client = useRenderFetch("/api/client");
ledger.write("evidence", [
...ledger.read("evidence"),
{ at: Date.now(), scenario: "mixed", note: `client:${String(client.title ?? "ok")}` },
]);
}
if (hint.read("pick") === "meta") {
useRouteMeta({ title: "Binder", description: "Scenario-bound" });
useRouteAssets({ font: "Warm Sans", image: "/hero.png" });
ledger.write("evidence", [
...ledger.read("evidence"),
{ at: Date.now(), scenario: "meta", note: "meta+assets published" },
]);
}
// Publish binder contract as a state service.
useRenderEffect(() => {
binderState.write("posture", gov.read("posture"));
binderState.write("lane", gov.read("lane"));
binderState.write("tab", hint.read("tab"));
binderState.write("pick", hint.read("pick"));
binderState.write("hits", cache.current.hits);
binderState.write("lastScenario", cache.current.lastScenario);
binderState.write("evidenceCount", ledger.read("evidence").length);
});
return (
<AwaitBoundary fallback={<div>Loading scenario binder...</div>}>
<section data-posture={gov.read("posture")} data-lane={gov.read("lane")}>
<header>
<strong>Scenario Binder</strong>
<div>Pick: {hint.read("pick")} / Hits: {cache.current.hits}</div>
<nav>
<button onClick={() => hint.write("tab", "scenarios")}>Scenarios</button>
<button onClick={() => hint.write("tab", "assets")}>Assets</button>
<button onClick={() => hint.write("tab", "evidence")}>Evidence</button>
</nav>
</header>
<main>
{hint.read("tab") === "scenarios" ? (
<ScenarioList pick={hint.read("pick")} onPick={(id) => hint.write("pick", id)} />
) : hint.read("tab") === "assets" ? (
<AssetsPanel />
) : (
<EvidencePanel state={binderState.readAll()} ledger={ledger.readAll()} />
)}
</main>
</section>
</AwaitBoundary>
);
}
function ScenarioList({ pick, onPick }: any) {
return (
<section>
<h3>Scenarios</h3>
<ul>
{scenarios.map((s) => (
<li key={s.id}>
<button onClick={() => onPick(s.id)}>
{s.name} {s.id === pick ? "(selected)" : ""}
</button>
</li>
))}
</ul>
</section>
);
}
function AssetsPanel() {
return (
<section>
<h3>Assets / metadata scenario</h3>
<p>This panel exists so file-router assets remain visible as a scenario.</p>
</section>
);
}
function EvidencePanel({ state, ledger }: any) {
return (
<section>
<h3>Evidence</h3>
<p>Binder contract (state service):</p>
<pre>{JSON.stringify(state, null, 2)}</pre>
<p>Ledger evidence (latest 12):</p>
<pre>{JSON.stringify({ ...ledger, evidence: ledger.evidence.slice(-12) }, null, 2)}</pre>
</section>
);
}
Scenario Binding Checklist
- Pair each API with a route scenario (not an abstract definition).
- Keep primitives shared across file routes and component routes.
- Prefer render-visible APIs so behavior is traceable.
- Store evidence as derived state so API usage becomes auditable.
- Publish a contract so nested segments agree on the active scenario.