Skip to content

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>
}
}
Compiled React 19 — this is the file in your repo
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 CustomerDetailPage

The 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.


  • A <Routes>/<Route path> table → the file path is the route
  • Number(sp.get("rev") ?? "1") coercion + defaultsquery rev: number = 1
  • sp.getAll("tags") array handlingquery tags: string[] = []
  • An untyped <Link to="string">RouteLink to typed to actual routes
  • Manual onMouseEnter prefetch wiringprefetch on hover
  • A useEffect (or Helmet) for the titlemeta { title }

The page compiles to a plain component over a tiny router runtime:

const __route = useRoute()
const id = __route.params.id
const rev = coerceQuery(__route.query.rev, "number", 1) // typed coercion + default
// + a generated routeManifest the file-based walker produces, and a useEffect for meta.title

useRoute / 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.