🌈

Remix v2 からReact Router v7 へupgradeをする

に公開

初めに

お疲れ様です。gontaです。
表題の通り、immedioのプロジェクトの一つでRemix v2 から React Router V7にアップデートの対応をしました。

基本はドキュメント通りに対応をしたらいいのですが、ところどころ導入をしているライブラリによってうまくいかないところがあったので、そちらについても記載をしていきます。

https://react-router-docs-ja.techtalk.jp/upgrading/remix#3-packagejson-の-scripts-を変更する

前提

下記のversionを使っていました。

  • "react": "^18.2.0",
  • "react-dom": "^18.2.7",
  • "@remix-run/node": "^2.11.1",
  • "@remix-run/react": "^2.11.1",
  • "@remix-run/serve": "^2.11.1",
  • "@sentry/remix": "^8.32.0",
  • "remix-routes": "^1.7.7",
  • "remix-utils": "^7.7.0",
  • "@remix-run/dev": "^2.11.1",

codemod を実行する

下記を実行して全体のpackageを変更します。

npx codemod remix/2/react-router/upgrade

上記を実行をすると、@remix-run/のpackageが@react-router/に変わります。
^7.0.0のversionがinstallをされると思うので、下記で最新のversionをinstallします。い6/10時点でlatestで実行をすると、^7.6.2でinstallをされています。

bun i @react-router/serve@latest @react-router/node@latest react-router@latest @react-router/dev@latest @react-router/dev@latest

package.jsonの中身をinstallする

package.json の scripts を変更する

これも基本ドキュメント通りです。

https://react-router-docs-ja.techtalk.jp/upgrading/remix#3-packagejson-の-scripts-を変更する

細かい点はプロジェクトに合わせてカスタマイズをしてください。

package.json
"dev": "react-router dev",
"build": "react-router build",
"start": "react-router-serve build/server/index.js",
"typecheck": "react-router typegen && tsc --build --noEmit",

routes.ts ファイルを追加する

immedioではremix-flat-routesを使っており、ドキュメントには記載がありません。
routes.ts内で使用するRemixのroutes設定オプションのアダプターをinstallします。

bun i @react-router/remix-config-routes-adapter

こちらを参考にさせていただきました。

https://remix.org.cn/resources/remix-flat-routes#-react-router-v7-support

app/routes.ts
import { remixConfigRoutes } from "@react-router/remix-config-routes-adapter";
import { flatRoutes } from "remix-flat-routes";

export default remixConfigRoutes((defineRoutes) =>
	flatRoutes("routes", defineRoutes),
);

React Router 構成を追加する

こちらも基本ドキュメント通りです。

https://react-router-docs-ja.techtalk.jp/upgrading/remix#5-react-router-構成を追加する

react-router.config.ts を作成

こちらもドキュメント通りに対応します。

touch react-router.config.ts
react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
} satisfies Config;

vite.config.ts

下記は修正後の一部実装です。

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import { remixDevTools } from "remix-development-tools";
import { remixRoutes } from "remix-routes/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [
    remixDevTools(),
    reactRouter(), // routes.tsを見て解釈をしているいるっぽい
    tsconfigPaths(),
    remixRoutes(),
  ],
  // ...
});

型安全性を有効にする

こちらもドキュメント通りに対応します。

.react-router/ を .gitignore に追加する

.gitignore
.react-router/

tsconfig.tsを更新する

こちらも基本はドキュメント通りに対応します。

  • include フィールドの .react-router/types/**/* パス
  • types フィールドの適切な @react-router/* パッケージ
  • 相対インポートを簡略化するための rootDirsを追加

追加対応でpluginsに@react-router/devを追加をし、routesが変更される度に型情報の自動生成が行われるようにします。下記を参考にさせていただきました。

https://zenn.dev/soartec_lab/articles/15512c4b9e6444

tsconfig.ts
{
  "include": [
    /* ... */
+   ".react-router/types/**/*"
  ],
  "compilerOptions": {
-   "types": ["@remix-run/node", "vite/client"],
+   "types": ["@react-router/node", "vite/client"],
    /* ... */
+   "rootDirs": [".", "./.react-router/types"],
+.  "plugins": [{ "name": "@react-router/dev" }], //
  }
}

これで、下記を実行をする

react-router typegen

そして下記がimportをすることができるようになります。今回はこちらのRouteへの変更処理は行いませんでした。

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

エントリファイル内のコンポーネントの名前を変更する

こちらもドキュメント通りです。
codemodを実行したときにコンポーネントの名前は自動で変わっていました。

https://react-router-docs-ja.techtalk.jp/upgrading/remix#8-エントリファイル内のコンポーネントの名前を変更する

entry.client.tsxを調整

entry.client.tsx
- import { RemixBrowser } from "@remix-run/react";
+ import { HydratedRouter } from "react-router/dom";
 
hydrateRoot(
  document,
  <StrictMode>
-   <RemixBrowser />
+   <HydratedRouter />
  </StrictMode>,
);

entry.server.tsxを調整

entry.server.tsx
- import { RemixServer } from "@remix-run/react";
+ import { ServerRouter } from "react-router";
 
- <RemixServer context={remixContext} url={request.url} />,
+ <ServerRouter context={remixContext} url={request.url} />,

自分はabortDelayが型エラーが出ていた。
react router v7でfutureフラグが全て有効化をされるとのことで、ABORT_DELAYも置き換えます。

https://remix-docs-ja.techtalk.jp/start/future-flags#v3_singlefetch

entry.server.tsx
- const ABORT_DELAY = 5000;
+ // 5 秒後に保留中のすべての Promise を拒否/キャンセルします
+ export const streamTimeout = 5000;
 
// ...
 
function handleBrowserRequest(/* ... */) {
  return new Promise((resolve, reject) => {
    const { pipe, abort } = renderToPipeableStream(
      <RemixServer
        context={remixContext}
        url={request.url}
-        abortDelay={ABORT_DELAY}
      />,
      {
        onShellReady() {
          /* ... */
        },
        onShellError(error: unknown) {
          /* ... */
        },
        onError(error: unknown) {
          /* ... */
        },
      }
    );
 
-    setTimeout(abort, ABORT_DELAY);
+   // React レンダラーを 6 秒後に自動的にタイムアウトさせます。これにより、
+   // React が拒否された境界の内容をフラッシュするのに十分な時間が確保されます。
+   setTimeout(abort, streamTimeout + 1000);
  });
}

動作確認

ドキュメントに合わせて実装をできたので、起動をしてみるとエラーが出ました。
エラーログを見ると、react-router-domがうまくいっていないみたいです。

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
✘ [ERROR] No matching export in "node_modules/react-router/dist/development/index.mjs" for import "UNSAFE_logV6DeprecationWarnings"

    node_modules/react-router-dom/dist/index.js:13:36:
      13 │ ..., UNSAFE_logV6Depreca...
         ╵      ~~~~~~~~~~~~~~~~~~~

こちらを参考にreact-router-domをinstallする

bun i react-router-dom@latest        

この時点で型エラーは大量に出ると思うのですが、ランタイムでエラーが出ないことを確認をしました🎉

型エラーや、ライブラリの細かい対応をする

ここまででローカルを起動をして画面を確認することができました。
おそらく動作は基本的に問題ないと思うのですが、下記を対応をしています。

  • 導入をしているライブラリの非互換で型エラーが出ている。
  • React Router v7へのライブラリのupdate

Sentry

react router v7に移行に合わせて@sentry/remixに移行をしました。

Write a migration path from @sentry/remix to @sentry/react-router

githubのissueが出ている。@sentry/remixをつかって、いずれは@sentry/react-routerを使うということのような気がする

下記を参考
https://github.com/getsentry/sentry-javascript/issues/14519

こちらを参考に@sentry/remixで実装調整

各所のimportを修正する。

- import * as Sentry from "@sentry/remix";
+ import * as Sentry from "@sentry/react";

Sentry.initのintegrationsを修正をする。

https://docs.sentry.io/platforms/javascript/guides/react/features/react-router/v7/

entry.client.tsx
+ import {
+ 	createRoutesFromChildren,
+ 	matchRoutes,
+ 	useLocation,
+ 	useNavigationType,
+ } from "react-router";

// ...
  Sentry.init({
  // ...
    integrations: [
-     Sentry.browserTracingIntegration({
-       useEffect,
-       useLocation,
-       useMatches,
-     }),
+.    Sentry.reactRouterV7BrowserTracingIntegration({
+.      useEffect,
+.      useLocation,
+.      useNavigationType,
+.      createRoutesFromChildren,
+.      matchRoutes,
      }),
      // ...
    ],
});

remix-routesをsafe-routesに更新

immedioでは型安全なルーティングを実現をするために、remix-routesを導入をしていました。

ただ、今回のバージョンアップに合わせて、$paramsの型が| undefindのunion型になってしまっている。

https://github.com/yesmeck/safe-routes/releases/tag/v1.0.0

こちらgithubを確認をするとremix-routesはsafe-routesに改名をされ、upgradeへのリンクもありましたのでこれどおりに対応をします。

remix-routes has been renamed to safe-routes. If you are looking for the documentation of remix-routes, please refer to here.
Please refer to the upgrading guide if you are upgrading from remix-routes.

https://github.com/yesmeck/safe-routes?tab=readme-ov-file

先にpackageのsafe-routesをinstallします。

bun uninstall remix-routes && bun i safe-routes

この後に自分は全てエディタの一括置換でsafe-routesに置き換えました

vite.config.tsを編集します。

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
- import { remixRoutes } from 'remix/vite';
+ import { safeRoutes } from 'safe-routes/vite';
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [
    reactRouter(),
    tsconfigPaths(),
    sentryVitePlugin({
      org: "immedio",
      project: "immedio-box",
    }),
-   remixRoutes()
+   safeRoutes(),
  ],
  // 
  // 
});

package.jsonのtypecheckを修正をします。

- "typecheck": "react-router typegen && tsc --build --noEmit"
+ "typecheck": "react-router typegen && safe-routes typegen && tsc --build --noEmit"

下記を実行します。

bun run typecheck

すると$paramsが非推奨になっていることが確認をできました。

こちらはそのまま利用をできるところは一旦そのままにして別タスクで対応をする判断をしました。

remix-utilsのversionを上げる

jsonHashを使われていたところが各所でneverを返すようになっていました。
v8からreact router v7に対応をしている様子なのでversionを最新に上げました。

^7.7.0 => ^8.7.0 に上げました。

 bun i remix-utils@latest

https://github.com/sergiodxa/remix-utils/releases

これでjsonHashでnever型を返さないようになりました。
注意点として、jsonHashを使うとDate型がstring型に変わっていたのですが、そのままDate型として返ってくるのでこの点の修正を対応をしました。

一旦終了

お疲れ様です!
私はこちらの対応で今回のプロジェクトでは型エラーも各所の実行も問題がないことを確認しました。

後回しにしたこと

$paramsが非推奨になるので修正

各actionとloaderでRouteで下記のように$paramsを使わない実装に修正できそうです。

- import type { ActionFunctionArgs } from "react-router";
- import { $params } from "safe-routes";
+ import type { Route } from "./+types";

export async function action({ params }: Route.ActionArgs) {
  const { bookmarkId } = params;
  // ...
}
- import type { LoaderFunctionArgs } from "react-router";
- import { $params } from "safe-routes";
+ import type { Route } from "./+types";

export async function loader({ request, params }: Route.LoaderArgs) {
  const { id: contentId } = params;
  // ...
}

Route.ComponentProps の対応

コンポーネントで使っているuseParams についても下記のようにしてより型安全に実装ができるようにする

- import { useParams } from "react-router";
+ import type { Route } from "./+types";

- export default function Contents() {
+ export default function Contents({ params }: Route.ComponentProps) {
-   const params = useParams();
-   const { id } = $params("/contents/:id", params);
+   const { id } = params;
}

最後に

ここまでお読みいただきありがとうございました。
基本がドキュメントに沿った実装にはなりましたが、細かいライブラリを導入でうまくいかない点があったので勉強になりました。
まだRemix v2を使っている企業様もあると思いますが、React Router v7 へupgradeをするときに、こちらの記事が少しでもお役に立てれば幸いです。

immedioテックブログ

Discussion