@pyreon/router
v0.14.0
Published
Official router for Pyreon
Downloads
1,701
Readme
@pyreon/router
Type-safe client-side router for Pyreon with hash and history modes, nested routes, guards, loaders, and scroll restoration.
Install
bun add @pyreon/routerQuick Start
import { createRouter, RouterProvider, RouterView, RouterLink } from '@pyreon/router'
// Type-safe named navigation — route names are checked at compile time
const router = createRouter<'home' | 'user'>({
routes: [
{ path: '/', component: Home, name: 'home' },
{ path: '/user/:id', component: UserPage, name: 'user' },
{
path: '/admin',
component: AdminLayout,
children: [{ path: 'users', component: AdminUsers }],
},
{ path: '/old-path', redirect: '/new-path' },
{ path: '/dashboard', component: lazy(() => import('./Dashboard')) },
{ path: '(.*)', component: NotFound },
],
})
const App = () => (
<RouterProvider router={router}>
<RouterView />
</RouterProvider>
)Typed Params
Route parameters are inferred from path strings:
const route = useRoute<'/user/:id'>()
route().params.id // stringNamed Navigation
const router = useRouter()
router.push({ name: 'user', params: { id: '42' } })RouterLink
<RouterLink to="/user/42">Profile</RouterLink>
<RouterLink to={{ name: "user", params: { id: "42" } }}>Profile</RouterLink>Data Loaders
import { useLoaderData, prefetchLoaderData } from '@pyreon/router'
const data = useLoaderData<typeof loader>()API
Router Creation
createRouter(options: RouterOptions)-- create a router instancelazy(loader)-- define a lazily loaded route component
Hooks
useRouter()-- access the router instanceuseRoute<Path>()-- access the current resolved route with typed paramsuseLoaderData<T>()-- access data returned by a route loader
Components
RouterProvider-- provides router context to the treeRouterView-- renders the matched route componentRouterLink-- anchor element with client-side navigation
Utilities
resolveRoute(routes, path)-- match a path against route definitionsparseQuery(search)/parseQueryMulti(search)-- parse query stringsstringifyQuery(params)-- serialize query parametersbuildPath(pattern, params)-- build a path from a pattern and paramsfindRouteByName(routes, name)-- look up a named routeprefetchLoaderData(router, path)-- prefetch loader data for a pathserializeLoaderData(router)/hydrateLoaderData(router, data)-- SSR serialization
Types
ExtractParams, RouteMeta, ResolvedRoute, RouteRecord, RouterOptions, Router, NavigationGuard, AfterEachHook, ScrollBehaviorFn, LoaderContext, RouteLoaderFn
View Transitions
Route changes are wrapped in document.startViewTransition() automatically when the browser supports it. Opt out per-route with meta: { viewTransition: false }.
await router.push() / .replace() resolves once the DOM has committed to the new route -- specifically, when the ViewTransition's updateCallbackDone promise settles. It does NOT wait for the full animation (.finished, 200-300ms), because blocking every programmatic navigation on an animation is unacceptable.
| Promise | Resolves when | Router awaits? |
| --- | --- | --- |
| updateCallbackDone | Callback done; DOM swapped; state live | yes |
| ready | Snapshot captured, pseudo-elements ready | no -- .catch() only |
| finished | Full animation completed | no -- .catch() only |
afterEach hooks and scroll restoration fire after the VT callback completes, so they observe the new route state when invoked.
notFound()
Throw notFound() in a loader or component to render a 404 boundary:
import { notFound, NotFoundBoundary, RouterView } from '@pyreon/router'
// Route loader:
{ path: '/user/:id', component: UserPage, loader: async ({ params }) => {
const user = await fetchUser(params.id)
if (!user) notFound()
return user
}}
// App layout:
<NotFoundBoundary fallback={<NotFoundPage />}>
<RouterView />
</NotFoundBoundary>Pending Components
Show a skeleton while route loaders run:
{
path: '/dashboard',
component: Dashboard,
loader: fetchDashboardData,
pendingComponent: DashboardSkeleton,
pendingMs: 200, // delay before showing skeleton (avoid flash)
pendingMinMs: 500, // minimum display time (avoid flicker)
}Validated Search Params
Type-safe query string validation per route — works with Zod, Valibot, or plain functions:
import { useValidatedSearch } from '@pyreon/router'
// Route config:
{
path: '/search',
component: SearchPage,
validateSearch: (raw) => ({
page: Number(raw.page) || 1,
q: raw.q ?? '',
}),
}
// With Zod:
{
path: '/search',
component: SearchPage,
validateSearch: z.object({
page: z.coerce.number().default(1),
q: z.string().default(''),
}).parse,
}
// In component:
const search = useValidatedSearch<{ page: number; q: string }>()
search().page // number — typed + validated
search().q // string — typed + validatedStructural sharing: useValidatedSearch() returns the same object reference when the validated values haven't changed, preventing unnecessary downstream re-renders.
