Scenario-Bound APIs
Feb 07, 2023Last edited Feb 17, 202681 min read

Scenario-Bound APIs: Deep Dive #44

MC
Malik Chen

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:

  1. Behavior: what the user experiences (e.g. “a list that stays fresh”).
  2. Decision: what the route must decide (e.g. “cache vs network vs stale”).
  3. 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

  1. Pair each API with a route scenario (not an abstract definition).
  2. Keep primitives shared across file routes and component routes.
  3. Prefer render-visible APIs so behavior is traceable.
  4. Store evidence as derived state so API usage becomes auditable.
  5. Publish a contract so nested segments agree on the active scenario.