Routes that know their own types
A customer page at /customers/:id with a typed ?rev= query (a real number,
with a default), a back-link, a tab title, and prefetch-on-hover.
In React Router, the URL hands you strings: you coerce them, default them, and
hope your <Link to="…"> targets still exist after a refactor. In Reactra, the
route’s inputs are typed fields on the page.
React Router — the URL is stringly-typed
Section titled “React Router — the URL is stringly-typed”import { useParams, useSearchParams, Link } from "react-router-dom"import { useEffect } from "react"
function CustomerDetailPage() { const { id } = useParams() // string | undefined const [sp] = useSearchParams() const rev = Number(sp.get("rev") ?? "1") // coerce + default, by hand const returnTo = sp.get("returnTo") ?? undefined const tags = sp.getAll("tags") // string[]
useEffect(() => { document.title = `Customer ${id}` }, [id]) // title, by hand
return ( <main> <Link to="/">back</Link> {/* `to` is an unchecked string */} <p>id: {id} · rev: {rev}</p> </main> )}
// …and elsewhere, the route table:// <Routes>// <Route path="/customers/:id" element={<CustomerDetailPage />} />// </Routes>id is string | undefined. rev is whatever Number() makes of a string. A
typo in to="/customrs" compiles fine and 404s at runtime. Prefetch-on-hover is
yours to wire.
Reactra — the route’s inputs are typed fields
Section titled “Reactra — the route’s inputs are typed fields”import { RouteLink } from "../../routeManifest.generated"
export component CustomerDetailPage { // file: pages/customers/[id].tsx → /customers/:id param id: string // from the [id] segment query returnTo?: string query rev: number = 1 // a real number, default 1 — coerced for you query tags: string[] = [] // repeated ?tags=… → string[]
prefetch on hover // every RouteLink here prefetches on hover meta { title: `Customer ${id}` } // sets the tab title
view { <main> <RouteLink to="/">back</RouteLink> {/* `to` is checked against real routes */} <p>id: {id} · rev: {rev}</p> </main> }}import { RouteLink } from "../../routeManifest.generated"import { useEffect } from "react"import { coerceQuery, useRoute } from "@reactra/router"
export const CustomerDetailPage = () => { const __route = useRoute() const id = __route.params.id const returnTo = __route.query.returnTo const rev = coerceQuery(__route.query.rev, "number", 1) const tags = coerceQuery(__route.query.tags, "string[]", []) const __meta = { title: `Customer ${id}` } useEffect(() => { if (typeof document !== "undefined" && __meta.title != null) document.title = String(__meta.title) }, [__meta.title]) useEffect(() => { const h = globalThis.__REACTRA_TEST__; if (h) h.update("CustomerDetailPage", {}) }) return (<> <main> <RouteLink to="/">back</RouteLink> {/* `to` is checked against real routes */} <p>id: {id} · rev: {rev}</p> </main> </>)}export default CustomerDetailPageThe filename is the route (pages/customers/[id].tsx → /customers/:id).
id is string. rev is a number that defaults to 1. RouteLink to only
accepts a real route id — a typo is a type error, not a 404.
What disappeared
Section titled “What disappeared”- A
<Routes>/<Route path>table → the file path is the route Number(sp.get("rev") ?? "1")coercion + defaults →query rev: number = 1sp.getAll("tags")array handling →query tags: string[] = []- An untyped
<Link to="string">→RouteLink totyped to actual routes - Manual
onMouseEnterprefetch wiring →prefetch on hover - A
useEffect(or Helmet) for the title →meta { title }
It’s not a new runtime
Section titled “It’s not a new runtime”The page compiles to a plain component over a tiny router runtime:
const __route = useRoute()const id = __route.params.idconst rev = coerceQuery(__route.query.rev, "number", 1) // typed coercion + default// + a generated routeManifest the file-based walker produces, and a useEffect for meta.titleuseRoute / coerceQuery / RouteLink are ordinary exports from
@reactra/router; the route table is a generated file you can read.
The URL has a type. Your links can’t lie.