Get Started
@k8ordo/router is a router for the pathname of the URL. You write one route table, and the matching, the typed links and the navigation all follow from it. This page builds a small application end to end: the table, mounting it in the browser, and checking links against it in the type system.
What it owns
This package owns one part of the URL: the pathname. Search params and history-entry state belong to @k8ordo/state, and the boundary between the two is the URL’s own ?.
| Part of the URL | Example | Owned by |
|---|---|---|
| Pathname | /products/42 | @k8ordo/router |
| Search | ?sort=price | @k8ordo/state |
| History-entry state | (not in the URL) | @k8ordo/state |
| Fragment | #reviews | The browser (on a page change, the router scrolls to it) |
There is no useSearchParams, and the raw search string is never handed out. A component that reads one field of the search should re-render when that field changes and not otherwise, which is a job for keyed subscriptions, not for a router. @k8ordo/state
It does not fetch either. There is no loader, no route-level data API and no cache. Data belongs to the component that needs it — use() and <Suspense> in a client application, the server under the framework — and a router that owned fetching would be a second answer to a question React already answers.
Installation
In an application that renders in the browser, install it alongside React and React DOM. An application on @k8ordo/static or @k8ordo/server depends on this package directly as well.
npm install @k8ordo/router react react-domThe peer dependencies are below. There are no runtime dependencies.
react>= 19.3.0typescript>= 7.0.2 and@types/react>= 19.3.0 (both optional; needed for the shipped type declarations)- The Navigation API and URLPattern are the platform’s. Both have reached Baseline newly available, and no polyfill or fallback ships with the package.
- It ships as ESM only.
Build a minimal application
Four steps: write the table, mount it, link from the pages, and check the links against the table.
1. The route table
Pass defineRoutes a table keyed by pathname patterns. A branch under / adds nothing to the URL, and its layout wraps every page. Matching takes the first pattern that fits in the order written, so /*, which fits anything, goes last.
// src/routes.ts
import { defineRoutes } from '@k8ordo/router';
import { Home } from './pages/home';
import { NotFound } from './pages/not-found';
import { ProductList } from './pages/product-list';
import { ProductPage } from './pages/product-page';
import { RootLayout } from './root-layout';
export const routes = defineRoutes({
'/': {
layout: RootLayout,
children: {
'/': Home,
'/products': ProductList,
'/products/:id': ProductPage,
'/*': NotFound,
},
},
});2. Mount it
Put <Router routes> at the root of the application, once. A layout renders what it wraps through <Outlet />. <Router> reads where the browser is when it mounts, so it is for an application that renders in the browser; to render on a server or at build time, use @k8ordo/static or @k8ordo/server.
// src/main.tsx
import { Router } from '@k8ordo/router';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { routes } from './routes';
const root = document.querySelector('#root');
if (root === null) {
throw new Error('#root is missing');
}
createRoot(root).render(
<StrictMode>
<Router routes={routes} />
</StrictMode>,
);// src/root-layout.tsx
import { href, Outlet } from '@k8ordo/router';
export function RootLayout() {
return (
<>
<nav>
<a href={href('/')}>Home</a>
<a href={href('/products')}>Products</a>
</nav>
<main>
<Outlet />
</main>
</>
);
}3. Link from the pages
Pages never import the table. href, navigateTo and useParams all take the pattern as a string. Only <Router> holds the table’s value, so the cycle of a table importing pages that import the table cannot form.
// src/pages/product-list.tsx
import { href } from '@k8ordo/router';
const products = [
{ id: '1', name: 'Desk lamp' },
{ id: '2', name: 'Notebook' },
];
export function ProductList() {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<a href={href('/products/:id', { id: product.id })}>
{product.name}
</a>
</li>
))}
</ul>
);
}// src/pages/product-page.tsx
import { href, navigateTo, useParams } from '@k8ordo/router';
export function ProductPage() {
const { id } = useParams('/products/:id');
return (
<article>
<h1>Product {id}</h1>
<a href={href('/products')}>Back to the list</a>
<button
onClick={() => {
navigateTo('/');
}}
type="button"
>
Home
</button>
</article>
);
}A link is a plain <a>. Under the Navigation API the router receives the navigate event the browser sends anyway, so the anchor is already a client navigation. The type useParams returns is inferred from the pattern string, so id is a string.
// src/pages/home.tsx
export function Home() {
return <h1>Home</h1>;
}// src/pages/not-found.tsx
import { usePathname } from '@k8ordo/router';
export function NotFound() {
return <p>Nothing at {usePathname()}</p>;
}4. Check links against the table
Params are already inferred from the pattern string, so forgetting :id fails to compile. To check the pattern itself against the real table, augment Register once.
// types/k8ordo-router.d.ts
import type { routes } from '../src/routes';
declare module '@k8ordo/router' {
interface Register {
routes: typeof routes;
}
}Add the declaration file to include in tsconfig.json.
{
"include": ["src", "types"]
}import { href } from '@k8ordo/router';
href('/products/:id', { id: '1' });
// @ts-expect-error
href('/product/:id', { id: '1' });
// @ts-expect-error
href('/products/:id');Now a pattern the table does not have is a type error too. Before the augmentation any string starting with / passes. Augment only in an application: a library that did it would impose its table on every consumer.
Next steps
- The table’s grammar: params, wildcards, groups, order and error boundaries
- Links,
navigateTo, and the hooks that read the location - What navigation guarantees, animating page changes, and testing
- The router under
@k8ordo/staticand@k8ordo/server @k8ordo/static, which renders every page at build time@k8ordo/server, which renders per request