Get Started
@k8ordo/router は URL の pathname を扱うルーターです。ルート表を 1 つ書けば、マッチング・型付きリンク・ナビゲーションがそこから決まります。このページでは小さなアプリを、表を書く・ブラウザでマウントする・リンクを表と型で照合する、の順に最後まで通して作ります。
担当する範囲
このパッケージが持つのは URL のうち pathname だけです。search params と履歴エントリの状態は @k8ordo/state が持ち、境界は URL の ? と一致します。
| URL の部分 | 例 | 担当 |
|---|---|---|
| pathname | /products/42 | @k8ordo/router |
| search | ?sort=price | @k8ordo/state |
| 履歴エントリの状態 | (URL に現れない) | @k8ordo/state |
| fragment | #reviews | ブラウザ(ページが変わるときはルーターがその位置へスクロール) |
useSearchParams はありません。search の生の文字列も渡しません。search の 1 項目だけを読むコンポーネントは、その項目が変わったときだけ再描画されるべきで、それはキー単位で購読する状態管理の仕事だからです。 @k8ordo/state
データの取得もしません。loader もルート単位のデータ API もキャッシュもありません。データは必要とするコンポーネントのもので、クライアントアプリなら use() と <Suspense>、フレームワークの下ならサーバーがその答えです。ルーターが取得まで持つと、React がすでに答えている問いに 2 つ目の答えを作ることになります。
インストール
ブラウザで描画するアプリでは、React と React DOM と一緒に入れます。@k8ordo/static や @k8ordo/server を使うアプリも、このパッケージを直接の依存に持ちます。
npm install @k8ordo/router react react-dompeer dependencies は次のとおりです。ランタイムの依存はありません。
react>= 19.3.0typescript>= 7.0.2 と@types/react>= 19.3.0(どちらも任意。同梱の型定義を使うときに必要)- Navigation API と URLPattern はプラットフォームのものを使います。どちらも Baseline の newly available に達しており、polyfill もフォールバックも同梱しません。
- ESM のみで配布されます。
最小のアプリを作る
表を書く、マウントする、ページからリンクする、表と型で照合する、の 4 段階です。
1. ルート表
defineRoutes に pathname パターンをキーとした表を渡します。/ に置いた branch は URL に何も足さず、layout がすべてのページを包みます。照合は書いた順で最初に合ったものが勝つので、何にでも合う /* は最後に置きます。
// 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. マウントする
<Router routes> をアプリの根に 1 度だけ置きます。レイアウトは自分が包む中身を <Outlet /> で描きます。<Router> はマウントした時点でブラウザの現在地を読むので、ブラウザで描画するアプリのためのものです。サーバーやビルド時に描画するなら @k8ordo/static か @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. ページからリンクする
ページは表を import しません。href にも navigateTo にも useParams にも、パターンの文字列を渡すだけです。表を持つのは <Router> だけなので、「表がページを import し、ページが表を import する」循環は構造的に生まれません。
// 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> です。Navigation API の下ではブラウザが送る navigate イベントをルーターが受け取るので、<a> がそのままクライアント遷移になります。useParams の戻り値の型はパターン文字列から推論され、id は 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. 表と型で照合する
ここまででも params はパターン文字列から推論されるので、:id を渡し忘れればコンパイルで落ちます。パターンそのものを実際の表と照合するには、Register を 1 度だけ augment します。
// types/k8ordo-router.d.ts
import type { routes } from '../src/routes';
declare module '@k8ordo/router' {
interface Register {
routes: typeof routes;
}
}宣言ファイルは tsconfig.json の include に入れます。
{
"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');これで表に無いパターンも型エラーになります。augment の前は / で始まる任意の文字列が通ります。augment はアプリでだけ行います。ライブラリが行うと、その表をすべての利用者に押し付けることになります。