Router
@domphy/router provides type-safe routing for Domphy apps: nested route trees, path params, validated search params, loaders with caching, redirects, navigation blocking, scroll restoration, and SSR.
It is a 1-1 port of @tanstack/router-core v1.171.13 (MIT, © Tanner Linsley and the TanStack team). The source is kept byte-identical to upstream, so the entire TanStack Router reference applies as-is, and future upstream versions can be diffed and merged directly. A thin adapter adds createRouter, createRoute, createRootRoute, and createRootRouteWithContext, and the @tanstack/history layer is re-exported so no separate install is needed.
Like the rest of Domphy, it is framework-agnostic — the bridge to the UI is plain toState.
Install
npm install @domphy/router
Live Examples
Basic navigation:
Route loaders:
Core Concepts
- Route tree — built from
createRootRoute()andcreateRoute(), composed withaddChildren. Every route knows its parent viagetParentRoute, which is what makes params, search, and loader data fully typed. - Router —
createRouter({ routeTree, history })owns matching, navigation, loading, and caching.router.stateis the single source of truth. - Matches —
router.state.matchesis the array of matched routes for the current location, ordered root → leaf. Each match carriesparams,search,loaderData, andstatus. - History —
createBrowserHistory(),createHashHistory(), orcreateMemoryHistory()decide how locations map to the URL (or to memory, for tests and SSR).
The Bridge Pattern
Domphy has no router primitive by design — routing is a state problem, and state lives outside the UI. The router manages location and match state; toState pushes it into the UI:
import { type DomphyElement, toState } from "@domphy/core"
import {
createRouter, createRoute, createRootRoute, createMemoryHistory,
type AnyRouteMatch,
} from "@domphy/router"
const rootRoute = createRootRoute()
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: "/" })
const postRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/posts/$postId",
loader: ({ params }) => fetchPost(params.postId),
})
const routeTree = rootRoute.addChildren([indexRoute, postRoute])
const router = createRouter({ routeTree, history: createMemoryHistory({ initialEntries: ["/"] }) })
// Bridge: router state -> Domphy states
const matches = toState<Array<AnyRouteMatch>>([])
const pathname = toState("/")
function syncRouterState() {
matches.set(router.state.matches)
pathname.set(router.state.location.pathname)
}
router.subscribe("onResolved", syncRouterState)
await router.load()
syncRouterState()
The UI reads the states reactively — nothing router-specific leaks into elements:
const App: DomphyElement<"main"> = {
main: (l) => {
const match = matches.get(l).find((m) => m.routeId === postRoute.id)
if (!match) return [{ p: "Welcome" }]
return [{ h1: match.loaderData.title }, { p: match.loaderData.body }]
},
}
Links
Render real <a> elements with real hrefs, but intercept the click so navigation stays client-side:
const link = (to: string, label: string): DomphyElement<"a"> => ({
a: label,
href: router.buildLocation({ to }).href,
onClick: (e) => {
e.preventDefault()
router.navigate({ to })
},
})
See Navigation for active links, history types, and blocking.
What To Read Next
- Route Trees for
createRoute, path params, wildcards, and nested layouts - Navigation for
navigate,buildLocation, link patterns, and blocking - Search Params for
validateSearchand search middleware - Data Loading for loaders, caching,
redirect(), andnotFound() - SSR for the server and client SSR entries
- API Reference for the full export list