タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

シンプルさに極振りしたSPAルーターが欲しかった

Sponsored link

古くはReactRouterとか、Remixとか両者の統合とか(コロコロコロコロ変わるからもうよくわかってない)……
いまはPages RouterとかApp Routerとか(複雑すぎてわかってない)……

とにかくルーティングライブラリは複雑な割に破壊的変更がはいりまくりまくって本当にしんどいです。
ていうかあたしディレクトリ構造とページを一対一にしたいモチベーション全然わかんないし……
主戦場がちょっと特殊なのでRSC採用する必要ないし……

というかRSCとかその辺とルーティングって密結合にしないとダメなの?なんかmiddleware的な仕組みでプラガブルにできないん(マジでよくわかってない)?

とにかく、シンプルさに極振りしたSPAルーターが欲しかった。
URLをみて、レンダリングを制御する。マジでそれだけやってて欲しい。あとnpmの余計な依存とか連れてこないで欲しい。
そういう夜もある。
ただし型安全性、テメーは必要です。

とにかくシンプルなやつを採用すればメンテが止まっても自分で同じ様なモン書けばなんとかなる。シンプルにまとめればルーターなんて数時間もあれば書けるんだから。
……そういうモチベーションでRoconを採用してました。

そしたら更新が止まったので「そろそろなんとかしねえとな」ということで、書きました。

github.com

できたてほやほやなのでまだ割と問題はありそう。

使い方

簡単なメソッドチェイン的簡易DSLでルーティングを定義できます。

なんと!型パズルパワーでパスのパラーメーターの型推論がいまならファッキン無料!!

/users/:userId みたいなパスを作れば、レンダー部分は ({userId}) => ReactNode が推論されます。やったね!
Searchパラメーターは常にoptionalとして ({$search: {params}}) => ReactNode みたいな感じで推論されます。
TypeScriptおもすれ〜

// Define your routes with type-safe parameters
const router = route('root', '/', () => <div>Home Page</div>)
  .add('users', '/users', () => <div>Users List</div>)
  .add('userDetail', '/users/:userId', (params) => (
    <div>User Details for ID: {params.userId}</div>
  ))
  .add('userPosts', '/users/:userId/posts/:postId', (params) => (
    <div>
      Posts for user {params.userId}
      {params.postId && <span> - Viewing post {params.postId}</span>}
    </div>
  ))
  .add('searchProducts', '/products?query=query&category=category&sort=sort', (params) => (
    <div>
      Product search results
      {params.$search.query && <span> for: {params.$search.query}</span>}
      {params.$search.category && <span> in category: {params.$search.category}</span>}
      {params.$search.sort && <span> sorted by: {params.$search.sort}</span>}
    </div>
  ));

ルーティング定義だけ型安全だと片手落ちですよね。
大丈夫です。
型パズルの闇の力でナビゲーション部分も補完と型チェックが効きます。

// Create navigation hooks
const { useNavigate, useRedirect, Link } = routingHooksFactory(router);

function Navigation() {
  const navigate = useNavigate();
  
  return (
    <nav>
      {/* Regular links with automatic navigation */}
      <Link route="root">Home</Link>
      <Link route="users">Users</Link>
      <Link route="userDetail" args={{ userId: "123" }}>User 123</Link>
      
      {/* Link with search parameters */}
      <Link route="searchProducts" args={{$search: {query: "laptop", category: "electronics", sort: "price" }}}>
        Search Electronics
      </Link>
      
      {/* Programmatic navigation */}
      <button onClick={() => navigate('userPosts', { userId: "123", postId: "456" })}>
        View User 123's Post 456
      </button>
      
      {/* Programmatic navigation with search parameters */}
      <button onClick={() => navigate('searchProducts', {$search: {query: "shoes", sort: "newest" }})}>
        Search for shoes
      </button>
    </nav>
  );
}

ところでルーター定義から routingHooksFactory でhooksを動的にひり出しているわけなんですが、こういうスタイルでAPI開発してる人をみたことがありません。
なんかルール違反を犯していないか心配です。

ルールといえばそう、このライブラリにも簡単なルールがあります。
historyに依存するAPIは上位にProviderを配置しましょう。

function Main() {
  const history = createBrowserHistory();
  return (
    <RouteProvider history={history}>
      <App />
    </RouteProvider>
  );
}

ね、簡単でしょ?

残タスク

  • APIが最低限すぎるのでまだいくつかっても良さそう
  • historyが無意味な大量の依存を連れてくるのでdependenciesにいれたくないが、enumを使っているせいで一部型の非互換が生じる
  • historyが品質の問題を抱えている、historyのメンテが止まっている。このふたつを解決するforkが欲しい
  • 雑なユニットテストしか書いてない
  • paramsへの変換処理(zod使ったりとか)をおけるmiddleware的な仕組みがあると嬉しいかもしれない
  • fetcherを持たせたい
  • suspenseとの連携が書きやすくなっているとうれしいかしらん?
  • RSCマジで何も調べたことがないので調べる

以上。

こんくらい簡単なルーターならだいたい300行もあれば書けるし、大抵のプロジェクトってこの程度の低機能ライブラリでいんじゃない?っておもうんですよね。
まぁNextっていうチェーンソーで木彫り人形を作れるくらいの達人ならいいんでしょうが……しかしああいう賢いフレームワークはアップデートについていくのが大変ですからね。

ま、こういう小規模ライブラリは簡単に更新が停止する問題があります。
子育ての間を縫って週末にセコセコ作ったんで、このモチベーションが続くかはちょっと謎なところがあります。
でも300行程度のライブラリなんで、僕が投げ出しても変わりを作るのはチョー簡単だし、まぁそういう視点でライブラリ選定していくのもありなんじゃない?
React-Routerの全機能を使いこなしていて、アップデートでAPIがカッチョよくなったことの恩恵受けたことある人おる????
そんなことをおもってます。

ルーティングとかFormのステート管理程度の作業にデカいライブラリいれるの嫌だよ俺は。

とにかくライブラリはね、規模が小さくて自由で、なんというか救われてなくちゃあダメなんだ 小さくて依存が少なくてプラガブルで……

以上。