@k8ordo/router

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-dom

peer dependencies は次のとおりです。ランタイムの依存はありません。

  • react >= 19.3.0
  • typescript >= 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 の戻り値の型はパターン文字列から推論され、idstring です。

// 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.jsoninclude に入れます。

{
  "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 はアプリでだけ行います。ライブラリが行うと、その表をすべての利用者に押し付けることになります。

次に読む