Router setup
The pattern
Define routes at module scope, wrap the app in a provider, mount <RouterView />:
import { createRouter, RouterProvider, RouterView, RouterLink } from '@pyreon/router'
const router = createRouter({
routes: [
{ path: '/', name: 'home', component: Home },
{
path: '/user/:id',
name: 'user',
component: UserPage,
loader: async ({ params }) => api.fetchUser(params.id),
loaderKey: ({ params }) => `user-${params.id}`, // for cache identity
},
{ path: '/admin', name: 'admin', component: Admin, guard: adminOnly },
],
})
function App() {
return (
<RouterProvider router={router}>
<nav>
<RouterLink name="home">Home</RouterLink>
<RouterLink name="user" params={{ id: '42' }}>User 42</RouterLink>
</nav>
<RouterView />
</RouterProvider>
)
}For typed named navigation, constrain the Router generic:
const router = createRouter<'home' | 'user' | 'admin'>({ routes })
router.push({ name: 'home' }) // ok
router.push({ name: 'typo' }) // TS error at compile timeKey APIs
router.push({ name, params?, search? })— navigate forward (history entry added)router.replace(...)— navigate without adding a history entryuseRoute()— signal that reads the current resolved routeuseIsActive(path, exact?)—Signal<boolean>for active-link stylinguseTypedSearchParams({ page: 'number' })— coerced + typed query paramsrouter.invalidateLoader(key?)— clear loader cache entries
await router.push(...) resolves AFTER the route's view-transition callback commits, so the next line can read the new route immediately.
Why
Pyreon's router is context-based + signal-driven. Routes are resolved into Signal<ResolvedRoute> so RouterView re-renders only when the route changes, not when unrelated state changes. Typed routes catch dead navigation calls at compile time — a rename in the routes array surfaces as a TypeScript error across every router.push / <RouterLink name> site.
Anti-pattern
// BROKEN — mutating window.location directly bypasses the router
window.location.pathname = '/user/42' // full page reload, loses SPA state
// Correct:
router.push({ name: 'user', params: { id: '42' } })// BROKEN — useRoute() called at module scope
const route = useRoute() // hooks require an active component setup context
// Correct: call inside a component body
function UserPage() {
const route = useRoute()
return <div>{() => route().params.id}</div>
}// BROKEN — router.push inside the render body triggers infinite loops
function Redirect() {
router.push({ name: 'home' }) // renders, pushes, renders, pushes…
return null
}
// Correct: push from onMount or an event handler (detector:
// no-imperative-navigate-in-render catches this in the lint rule)
function Redirect() {
onMount(() => router.push({ name: 'home' }))
return null
}Related
Reference API:
createRouter,useRoute,RouterView,RouterLink—get_api({ package: "router", symbol: "..." })Lint rule:
pyreon/no-imperative-navigate-in-renderAnti-pattern: "
const handleClick = () => router.push(…); handleClick()in render body"