白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Remixから見たReact Router v7との差分

こんばんは。
まだまだ暑いですが、果物などの旬の食べ物を見ると秋を感じますね。

さて、RemixがReact Router v7に合流してからしばらく経ちました。
そういえば改めて比較をしていなかったので、代表的な差異を見てみます。

移行は割と簡単らしい

まず大前提として、Remix v2でfuture flagsをすべて有効にした状態であれば、React Router v7(以降単にv7と記載します)への移行は非破壊的にできるそうです。

// vite.config.ts (Remix v2)
remix({
  future: {
    v3_fetcherPersist: true,
    v3_relativeSplatPath: true,
    v3_throwAbortReason: true,
    v3_singleFetch: true,
    v3_lazyRouteDiscovery: true,
  }
});

詳しい手順はReact Routerのサイトにありますが、基本的にはcodemodというツールを使うのが簡単とのことです。

reactrouter.com
app.codemod.com

今回は移行をしようという話ではないので省略します。

routes.ts による明示的なルーティング

さて、ここからは実際に差分を見ていきます。
まずはルーティングについてです。

Remixでは基本的にファイルの配置場所とファイル名によってルールベースでルーティングを行うファイルシステム(FS)ルーティングがベースでしたが、v7では app/routes.ts というファイルに明示的にルート定義をコードで書くのが基本になりました。
ちょっと面倒という説もありますが、個人的にはこちらのほうが好きですね。

// app/routes.ts
import { type RouteConfig, route, index, layout } from "@react-router/dev/routes";

export const routes: RouteConfig = [
  index("./home.tsx"),                       // "/" に対応(indexルート)
  route("about", "./about.tsx"),             // "/about" に対応
  layout("./auth/layout.tsx", [             // ネストされたレイアウトルート
    route("login", "./auth/login.tsx"),     // "/auth/login"
    route("register", "./auth/register.tsx")// "/auth/register"
  ]),
];

一方で、@react-router/fs-routesを使ってFSルーティングを使うこともできるとのことです。お好みで使い分けましょう。

import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;

loaderData prop

RemixではuseLoaderDataフックを使ってloaderからのデータを受け取っていました。
Single Fetchのfuture flagsがONだとnakedな型が付きます。

export default function Product() {
  const { product } = useLoaderData<typeof loader>(); 
  return <h1>{product.name}</h1>;
}

v7ではComponentPropsに直接loaderDataという形で型付きのデータが渡されてくるようになりました。
関数の呼び出しと変数の参照という結構大きな差異があるのですが、分割代入する分にはほぼ同等のコードにできます。
useLoaderDataの利用自体は可能なので、移行の際にはいったんそのままという選択肢もあるかと思います。

export default function Product({ loaderData }: Route.ComponentProps) {
  const { product } = loaderData; 
  return <h1>{product.name}</h1>;
}

Single Fetchについては以前の記事に書いたので良ければどうぞ。
shironeko.hateblo.jp

型の自動生成

前述のloaderDataの型はいつ作られるんだろうという話ですが、これは自動生成されます。
Viteプラグインがroutes.tsを監視し、開発サーバ実行中(react-router dev = npm run dev)に自動生成するという仕組みのようです。
型チェック(react-router typegen && tsc = npm run typecheck)でも実行されるので、CIではこっちが推奨ですかね。
「./+types/~」でlinterのエラーが消えない場合はtypecheckしてみると良いかと思います。

loaderやactionのデータの型が作られますが、URLからパラメタの型も作ってくれるのはすごいですね。

import type { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  // params.productIdが自動的に文字列型として推論される
  const product = await getProduct(params.productId);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  // loaderData.productが完全に型付けされる
  const { product } = loaderData; 
  return <h1>{product.name}</h1>;
}

react-router.config.ts の追加

react-router.config.tsという設定ファイルができました。
デフォルトではssr: trueという設定だけが書いてあります。
Remixではremix.config.tsとvite.config.tsに設定がばらけがちでしたが、react-router.config.tsにうまく集約されるようです。

reactrouter.com

ここではプリレンダリングの有効化なども設定できます。
これはパスの情報だし、app/routes.tsに寄せてもいいのではと思わなくもないですが。

export default {
  // プリレンダリング
  prerender: ["/", "/about", "/contact"],

  // 関数でも指定できるらしい
  prerender: async ({ getStaticPaths }) => {
    const paths = await getStaticPaths();
    return ["/", ...paths];
  },
} satisfies Config;

そんなに違和感はない

というわけで名前はReact Routerになってしまいましたが、(フレームワークモードとして使うなら)割と正統にRemixの後継として使えそうですね。
なんだか賛否あるようですが、個人的には結構シンプルなフレームワークで好きなので仲良くしたいところです。

それでは。