Routing
Oct 10, 2023214 min read
Navigation Treaties: Deep Dive #79
AV
Ariel Voss
Principal React Architect
Navigation treaties are contracts for transition posture:
- pacing (how long the shell may hold a transition window),
- focus rails (what gets focused and when),
- and scheduling permissions (scroll, announce, preload).
They exist for the same reason route treaties exist (#74): posture is policy, and policy needs a single negotiation surface. Treaties let you change posture without rewriting every navigation call site.
Required docs links:
Extra docs links for this entry:
Treaty doctrine for navigation
- A navigation request is negotiated into a grant posture.
- Terms are deterministic and recorded as evidence.
- Grants contain focus + pace window.
- Fallback grants are explicit (never implicit).
Worked Example: treaty:navigation (terms + grant + fallback)
Step 1: Types
export type NavTreatyTerm =
| { ok: true; id: string }
| { ok: false; id: string; reason: string };
export type NavTreatyGrant = {
focus: string;
paceMs: number;
};
export type NavTreaty = {
id: string;
terms: Array<(args: { to: string; shell: any }) => NavTreatyTerm>;
grant: (args: { to: string; shell: any }) => NavTreatyGrant;
fallback: (args: { to: string; shell: any; failed: NavTreatyTerm[] }) => NavTreatyGrant;
};
Step 2: Definition
export function defineNavTreaty(): NavTreaty {
return {
id: "treaty:navigation",
terms: [
({ to }) => (to ? { ok: true, id: "term.to.present" } : { ok: false, id: "term.to.present", reason: "missing" }),
({ shell }) =>
typeof shell.read("budgetMs") === "number"
? { ok: true, id: "term.budget.present" }
: { ok: false, id: "term.budget.present", reason: "missing" },
],
grant: ({ shell }) => ({ focus: "h1", paceMs: shell.read("budgetMs") }),
fallback: () => ({ focus: "main", paceMs: 0 }),
};
}
Step 3: Negotiate + write evidence
import { useLiveState } from "react";
export function NavigationTreatyShell() {
const nav = useLiveState("nav.shell", {
surface: "file-router",
active: "file:/",
budgetMs: 160,
focusTarget: "main",
rows: [] as any[],
});
const treaty = defineNavTreaty();
function navigate(to: string, cause: string) {
function navWrite(row: any) {
nav.write("rows", [...nav.read("rows"), { at: Date.now(), ...row }]);
}
navWrite({
to,
kind: "push",
requested: `treaty:${treaty.id}`,
granted: "pending",
focus: nav.read("focusTarget"),
surface: nav.read("surface"),
cause,
});
const results = treaty.terms.map((t) => t({ to, shell: nav }));
const failed = results.filter((r) => r.ok === false);
results.forEach((r) =>
navWrite({
to,
kind: "push",
requested: r.id,
granted: r.ok ? "ok" : "failed",
focus: nav.read("focusTarget"),
surface: nav.read("surface"),
cause: "shell:treaty-eval",
}),
);
const grant = failed.length ? treaty.fallback({ to, shell: nav, failed }) : treaty.grant({ to, shell: nav });
navWrite({
to,
kind: "push",
requested: "treaty.grant",
granted: failed.length ? "fallback" : "granted",
focus: grant.focus,
surface: nav.read("surface"),
cause: "shell:treaty-grant",
meta: { paceMs: grant.paceMs },
});
nav.write("active", to);
nav.write("focusTarget", grant.focus);
}
return (
<section data-shell="nav-treaty" data-active={nav.read("active")}>
<h1>Navigation Treaty</h1>
<div className="mt-4 flex gap-3">
<button onClick={() => navigate("file:/invoices", "nav:invoices")}>Go invoices</button>
<button onClick={() => navigate("", "nav:bad")}>Request empty destination</button>
</div>
<pre data-nav-rows>{JSON.stringify(nav.read("rows"), null, 2)}</pre>
</section>
);
}
Worked Example walkthrough (treaties shape posture, not destination)
Notice what the treaty does not decide:
- It doesn’t decide which route exists.
- It doesn’t decide which component renders.
It decides the posture of the transition: how long the shell is allowed to pace the grant (paceMs) and what the shell will focus (focus).
That’s what makes treaties team-scalable: you can change posture (pacing, focus rails, preload) without rewriting every route and every click handler.
Checklist
- Negotiate posture, don’t hardcode it.
- Record term results as evidence.
- Grant includes focus + pace.
- Fallback is explicit.