React Router v7 がついに Remix と合流したので情報整理

# はじめに
[React Conf 2024](https://ja.react.dev/blog/2024/05/22/react-conf-2024-recap) で[話題になった](https://remix.run/blog/merging-remix-and-react-router) [React Router v7](https://reactrouter.com/7.0.1/home) がついに2024/11/22に [Remix と合流](https://remix.run/blog/react-router-v7)しましたので公式ドキュメントを読みながら情報を整理します。

![react-router-remix-graphic.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/4b38077d-321c-1b9b-92da-6b659c9ec05e.jpeg)

Graphic by [Jacob Paris](https://x.com/jacobmparis/status/1790904647630151889)


https://remix.run/blog/react-router-v7



# React Router v7
[React Router v7 の公式ドキュメント](https://reactrouter.com/home)を読みながら、ポイントをまとめていきます。

__Non-breaking__
Upgrading from v6 to v7 is a non-breaking upgrade. Keep using React Router the same way you already do.

> v6 から v7 へのアップグレードは非破壊アップグレードです。これまでと同じように React Router を使い続けてください。


__Bridge to React 19__
All new bundling, server rendering, pre-rendering, and streaming features allow you bridge the gap from React 18 to 19 incrementally.
> 新しいバンドル、サーバーレンダリング、プリレンダリング、ストリーミング機能により、React 18 から 19 へのギャップを段階的に埋めることができます。

__Type Safety__
New typegen provides first class types for route params, loader data, actions, and more.
> 新しい typegen は、ルートパラメータ、ローダーデータ、アクションなどのファーストクラスの型を提供します。


## ライブラリとしての React Router

v7 でも引き続き、シンプルで宣言的なルーティングライブラリとして使用できます。URL をコンポーネントと一致させ、URL データへのアクセスを提供し、アプリ内を遷移します。

```JavaScript
ReactDOM.createRoot(root).render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard" element={<Dashboard />}>
        <Route index element={<RecentActivity />} />
        <Route path="project/:id" element={<Project />} />
      </Route>
    </Routes>
  </BrowserRouter>
);
```

（出典） [React Router as a framework](https://reactrouter.com/home#react-router-as-a-library)


## フレームワークとしての React Router

React Router が Remix と統合されたことで Remix の特徴を受け継ぎ __「Web 標準」__ に沿うものとなりました。基本的なデータの受け渡しの考え方は以下の図の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/1363d610-879f-28a0-2628-4557626c8cc6.png)

React Router を React フレームワークとして活用できるようになりました。一般的な Web プロジェクトに必要な以下のような多数の機能を提供します。


| 和訳・意訳 | 原文 |
|:-----------|:------------|
| Viteバンドラーと開発サーバーの統合 | Vite bundler and dev server integration |
| モジュールの即時適用 | hot module replacement |
| コード分割​ | code splitting |
| 型安全性を考慮したルート定義 | route conventions with type safety |
| ファイルシステムまたは設定に基づくルーティング | file system or config-based routing |
| 型安全性を考慮したデータ読み込み | data loading with type safety |
| 型安全性のあるアクション | actions with type safety |
| アクション後のページデータの自動再検証 | automatic revalidation of page data after actions |
| SSR、SPA、静的レンダリング | SSR, SPA, and static rendering strategies |
| 保留状態と楽観的UIのためのAPI | APIs for pending states and optimistic UI |
| デプロイメントアダプタ | deployment adapters |




__ルーティング__
各ルートをコード分割し、パラメーターとデータの型安全性を提供。ユーザーが遷移したときに保留中の状態にアクセスしてデータを自動的に読み込みます。


```JavaScript
import {
  type RouteConfig,
  route,
  index,
  layout,
  prefix,
} from "@react-router/dev/routes";

export default [
  index("./home.tsx"),
  route("about", "./about.tsx"),

  layout("./auth/layout.tsx", [
    route("login", "./auth/login.tsx"),
    route("register", "./auth/register.tsx"),
  ]),

  ...prefix("concerts", [
    index("./concerts/home.tsx"),
    route(":city", "./concerts/city.tsx"),
    route(":city/:id", "./concerts/show.tsx")
    route("trending", "./concerts/trending.tsx"),
  ]),
] satisfies RouteConfig;

```


__ローダー__
ローダーはルートコンポーネントにデータを提供します。

```JavaScript
// loaders provide data to components
export async function loader({ params }: Route.LoaderArgs) {
  const [show, isLiked] = await Promise.all([
    fakeDb.find("show", params.id),
    fakeIsLiked(params.city),
  ]);
  return { show, isLiked };
}

```

__コンポーネント__

コンポーネントは、プロパティとして渡されたローダーデータを使用して、ルーティングで指定された URL にてレンダリングされます。

```JavaScript
export default function Show({
  loaderData,
}: Route.ComponentProps) {
  const { show, isLiked } = loaderData;
  return (
    <div>
      <h1>{show.name}</h1>
      <p>{show.description}</p>

      <form method="post">
        <button
          type="submit"
          name="liked"
          value={isLiked ? 0 : 1}
        >
          {isLiked ? "Remove" : "Save"}
        </button>
      </form>
    </div>
  );
}

```

__アクション__
アクションによってデータを更新し、ページ上のすべてのデータの再検証をトリガーにして、UI を自動的に最新の状態に維持することができます。

```JavaScript
export async function action({
  request,
  params,
}: Route.LoaderArgs) {
  const formData = await request.formData();
  await fakeSetLikedShow(formData.get("liked"));
  return { ok: true };
}

```

（出典）[React Router as a framework](https://reactrouter.com/home#react-router-as-a-framework)


### loader の深堀り

[loader](https://reactrouter.com/start/framework/route-module#loader) / ルートローダーは、ルートコンポーネントがレンダリングされる前にデータを提供します。 これらはサーバーレンダリング時、またはプリレンダリングによるビルド時にのみサーバー上で呼び出されます。

```JavaScript
export async function loader() {
  return { message: "Hello, world!" };
}

export default function MyRoute({ loaderData }) {
  return <h1>{loaderData.message}</h1>;
}

```

[clientLoader](https://reactrouter.com/start/framework/route-module#clientloader) はブラウザでのみ呼び出されます。 `route loader` に加えてコンポーネントにデータ提供します。 `route loader` の代替としても使えます。


ブラウザでのみ呼び出されるルートクライアントローダーは、ルートローダーに加えて、あるいはルートローダーの代わりに、ルートコンポーネントにデータを提供します。


```JavaScript
export async function clientLoader({ serverLoader }) {
  // call the server loader
  const serverData = await serverLoader();
  // And/or fetch data on the client
  const data = getDataFromClient();
  // Return the data to expose through useLoaderData()
  return data;
}

```

`cliendLoader` は `hydrate` プロパティを設定することで、サーバ・レンダリングされたページの初期ページロード時のハイドレーションに参加することができます。

constとして使用することで、TypeScriptはclientLoader.hydrateの型がbooleanではなくtrueであることを推測します。 これにより、React RouterはclientLoader.hydrateの値に基づいてloaderDataの型を導出することができます。


```JavaScript
export async function clientLoader() {
  // ...
}
clientLoader.hydrate = true as const;

```

__クライアントデータの読み込み__

`clientLoader` クライアントでデータを取得するために使用されます。これは、ブラウザからのみデータを取得したいページやプロジェクト全体に役立ちます。

```app/product.tsx
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";

export async function clientLoader({
  params,
}: Route.ClientLoaderArgs) {
  const res = await fetch(`/api/products/${params.pid}`);
  const product = await res.json();
  return product;
}

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

```



__サーバーデータの読み込み__
サーバーレンダリングの際、`loader` は最初のページロードとクライアントナビゲーションの両方に使用されます。クライアントのナビゲーションは、ブラウザからサーバーへのReact Routerによる自動フェッチ / `fetch` を通じて `loader` を呼び出します。

ローダー関数 / `loader` はクライアント・バンドルから削除されるので、ブラウザに含まれることを気にすることなく、サーバーのみのAPIを使うことができます。


```app/product.tsx
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await fakeDb.getProduct(params.pid);
  return product;
}

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

```



他にも [Static Data Loading](https://reactrouter.com/start/framework/data-loading#static-data-loading) でプリレンダリングする URL を `react-router.config.ts` で指定することができます。


`loader` と `clientLoader` は一緒に使うことができます。 `loader` は最初のSSR(またはプリレンダリング)のためにサーバ上で使用され、`clientLoader` はその後のクライアントサイドのナビゲーションのために使用されます。


__（番外編：[Suspense](https://reactrouter.com/how-to/suspense
)）__
React Suspense を使用したスト​​リーミングにより、アプリは重要でないデータを延期し、UI レンダリングをブロック解除することで初期レンダリングを高速化できます。

React Router は、ローダーとアクションから Promise を返すことで React Suspense をサポートします。


（出典）[Route Module > loader](https://reactrouter.com/start/framework/route-module#loader) / [Route Module > clientLoader](https://reactrouter.com/start/framework/route-module#clientloader) / [Data Loading](https://reactrouter.com/start/framework/data-loading)


### action の深堀り


[action](https://reactrouter.com/start/framework/route-module#action) / ルートアクションを使用すると、`<Form>` 、`useFetcher` 、`useSubmit` から呼び出されたときに、ページ上のすべての読み込みデータを自動的に再検証して、サーバー側のデータ変更が可能になります。


```JavaScript
// route("/list", "./list.tsx")
import { Form } from "react-router";
import { TodoList } from "~/components/TodoList";

// このデータはアクションが完了した後に読み込まれます...
export async function loader() {
  const items = await fakeDb.getItems();
  return { items };
}

// ...ここでのリストは自動的に更新されます
export default function Items({ loaderData }) {
  return (
    <div>
      <List items={loaderData.items} />
      <Form method="post" navigate={false} action="/list">
        <input type="text" name="title" />
        <button type="submit">Create Todo</button>
      </Form>
    </div>
  );
}

export async function action({ request }) {
  const data = await request.formData();
  const todo = await fakeDb.addItem({
    title: data.get("title"),
  });
  return { ok: true };
}

```


[clientAction](https://reactrouter.com/start/framework/route-module#clientaction) はブラウザからのみ呼び出されるルートアクションです。


```JavaScript
export async function clientAction({ serverAction }) {
  fakeInvalidateClientSideCache();
  // 必要に応じてサーバーアクションを呼び出すことができる
  const data = await serverAction();
  return data;
}

```



__クライアントアクション__
クライアントアクションはブラウザー内でのみ実行され、両方が定義されている場合はサーバーアクションよりも優先されます。

```app/project.tsx
// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";

export async function clientAction({
  request,
}: Route.ClientActionArgs) {
  let formData = await request.formData();
  let title = await formData.get("title");
  let project = await someApi.updateProject({ title });
  return project;
}

export default function Project({
  actionData,
}: Route.ComponentProps) {
  return (
    <div>
      <h1>Project</h1>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">Submit</button>
      </Form>
      {actionData ? (
        <p>{actionData.title} updated</p>
      ) : null}
    </div>
  );
}

```


__サーバーアクション__
サーバーアクションはサーバー上でのみ実行され、クライアントバンドルからは削除されます。

```app/project.tsx
// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { fakeDb } from "../db";

export async function action({
  request,
}: Route.ActionArgs) {
  let formData = await request.formData();
  let title = await formData.get("title");
  let project = await fakeDb.updateProject({ title });
  return project;
}

export default function Project({
  actionData,
}: Route.ComponentProps) {
  return (
    <div>
      <h1>Project</h1>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">Submit</button>
      </Form>
      {actionData ? (
        <p>{actionData.title} updated</p>
      ) : null}
    </div>
  );
}

```

__アクションの呼び出し__
`action` はルートのパスと「post」メソッドを参照することにより `<Form>` と `useSubmit` を通じて宣言的に呼び出されます。

※ `<fetcher.Form>` と `fetcher.submit` も可

アクション後にはナビゲーションが実行され、ブラウザの履歴に追加されます。


例）`<Form>` でアクションを呼び出す。

```JavaScript
import { Form } from "react-router";

function SomeComponent() {
  return (
    <Form action="/projects/123" method="post">
      <input type="text" name="title" />
      <button type="submit">Submit</button>
    </Form>
  );
}
```

例）`useSubmit` でアクションを呼び出す
```JavaScript
import { useCallback } from "react";
import { useSubmit } from "react-router";
import { useFakeTimer } from "fake-lib";

function useQuizTimer() {
  let submit = useSubmit();

  let cb = useCallback(() => {
    submit(
      { quizTimedOut: true },
      { action: "/end-quiz", method: "post" }
    );
  }, []);

  let tenMinutes = 10 * 60 * 1000;
  useFakeTimer(tenMinutes, cb);
}

```

ナビゲーションを発生させず、ブラウザ履歴に残さなずアクションを送信するにはフェッチャーを使用します。

例）`useFetcher` でアクションを呼び出す

```JavaScript
import { useFetcher } from "react-router";

function Task() {
  let fetcher = useFetcher();
  let busy = fetcher.state !== "idle";

  return (
    <fetcher.Form method="post" action="/update-task/123">
      <input type="text" name="title" />
      <button type="submit">
        {busy ? "Saving..." : "Save"}
      </button>
    </fetcher.Form>
  );
}

```


（出典）[Route Module > action](https://reactrouter.com/start/framework/route-module#action) / [Route Module > clientAction](https://reactrouter.com/start/framework/route-module#clientaction) / [Actions](https://reactrouter.com/start/framework/actions)


### ErrorBoundary の深堀り

他のルートモジュールAPIが投げるとき、ルートコンポーネントの代わりにルートモジュールの `ErrorBoundary` がレンダリングされます。これは[Remix の ErrorBoundary を踏襲](https://remix.run/docs/en/main/route/error-boundary)しているようです。

Remixの `ErrorBoundary` コンポーネントは、通常の React のエラー境界と同じように動作しますが、いくつかの追加機能があります。 ルートコンポーネントにエラーがあると、`ErrorBoundary` はその場所にレンダリングされ、親ルートの内部にネストされます。 `ErrorBoundary` コンポーネントは、ルートのローダーやアクション関数でエラーが発生したときにもレンダリングされます。

最も一般的なユースケースは次のようなものです。
- UI エラーのトリガーとして意図的に 4xx レスポンスをスローする
  - ユーザーの入力に問題があった場合に 400 をスローする
  - 不正アクセスに対して 401 をスローする
  - リクエストされたデータが見つからない場合に 404 をスローする
- React は、レンダリング中にランタイムエラーと遭遇すると、意図せずに Error をスローすることがあります

スローされたオブジェクトを取得するには、`useRouteError` フックを使用します。 `Response` がスローされると、ステート/ステータステキスト/データフィールドを持つ `ErrorResponse` インスタンスに自動的にアンラップされます。スローされたレスポンスとスローされたエラーを区別するには、`isRouteErrorResponse` ユーティリティを使用します。



```JavaScript
import {
  isRouteErrorResponse,
  useRouteError,
} from "react-router";

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>
          {error.status} {error.statusText}
        </h1>
        <p>{error.data}</p>
      </div>
    );
  } else if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <p>The stack trace is:</p>
        <pre>{error.stack}</pre>
      </div>
    );
  } else {
    return <h1>Unknown Error</h1>;
  }
}

```

（出典）[Route Module > ErrorBoundary](https://reactrouter.com/start/framework/route-module#errorboundary) / [Remix > ErrorBoundary](https://remix.run/docs/en/main/route/error-boundary)


### Hydrate 深堀り

最初のページロードでは、ルートコンポーネントはクライアントローダが終了した後にのみレンダリングされます。エクスポートされた場合、ルートコンポーネントの代わりに `HydrateFallback` を直ちにレンダリングすることができます。


```JavaScript
export async function clientLoader() {
  const data = await fakeLoadLocalGameData();
  return data;
}

export function HydrateFallback() {
  return <p>Loading Game...</p>;
}

export default function Component({ loaderData }) {
  return <Game data={loaderData} />;
}

```


（出典）[Route Module > HydrateFallback](https://reactrouter.com/start/framework/route-module#hydratefallback)

### 他にも
[headers](https://reactrouter.com/start/framework/route-module#headers), [links](https://reactrouter.com/start/framework/route-module#headers), [meta](https://reactrouter.com/start/framework/route-module#headers) などがあり、SEOやアセット読み込みを意識した部品が提供されているようです。

[redirect](https://reactrouter.com/start/framework/navigating) や [useNavigate](https://reactrouter.com/start/framework/navigating) などは継続して利用できるようです。

[Pending UI](https://reactrouter.com/start/framework/pending-ui) は新しいページを開いたり、アクションにデータを送信した際の非同期処理を待つ保留状態を扱うものです。

### テスト

コンポーネントが `useLoaderData` や `<Link>` などを使用する場合、React Routerアプリのコンテキストでレンダリングする必要があります。`createRoutesStub` 関数は、コンポーネントを分離してテストするためのコンテキストを作成します。

`createRoutesStub` を使うことでローダー、アクション、コンポーネントを持つルートモジュールに似たオブジェクトを受け取りテストします。


```product-code.tsx
import { useActionData } from "react-router";

export function LoginForm() {
  const errors = useActionData();
  return (
    <Form method="post">
      <label>
        <input type="text" name="username" />
        {errors?.username && <div>{errors.username}</div>}
      </label>

      <label>
        <input type="password" name="password" />
        {errors?.password && <div>{errors.password}</div>}
      </label>

      <button type="submit">Login</button>
    </Form>
  );
}
```

```test-code.tsx
import { createRoutesStub } from "react-router";
import * as Test from "@testing-library/react";
import { LoginForm } from "./LoginForm";

test("LoginForm renders error messages", async () => {
  const USER_MESSAGE = "Username is required";
  const PASSWORD_MESSAGE = "Password is required";

  const Stub = createRoutesStub([
    {
      path: "/login",
      Component: LoginForm,
      action() {
        return {
          errors: {
            username: USER_MESSAGE,
            password: PASSWORD_MESSAGE,
          },
        };
      },
    },
  ]);

  // render the app stub at "/login"
  Test.render(<Stub initialEntries={["/login"]} />);

  // simulate interactions
  Test.user.click(screen.getByText("Login"));
  await Test.waitFor(() => screen.findByText(USER_MESSAGE));
  await Test.waitFor(() =>
    screen.findByText(PASSWORD_MESSAGE)
  );
});

```


（出典）[Testing](https://reactrouter.com/start/framework/testing)



## レンダリングの選択肢
React Router は３種類のレンダリングの選択肢を提供します。

CSR (Client Side Rendering)
SSR (Server Side Rendering)
SSG (Static Pre-rendering) *1 筆者はSSGと理解しました


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

export default {
  ssr: false,  // false で CSR, true で SSR
} satisfies Config;

```

`ssr:false` とすると常にクライアントサイドでレンダリングされます。SPA を構築する場合は、サーバーレンダリングを無効にしてください。

サーバーサイドレンダリングには、サーバーへのデプロイが必要です。`ssr:true` はグローバルな設定ですが、個々のルートで静的にプリレンダリングできます。また、ルートは `clientLoader` でクライアントデータのロードを使用し、UIの一部分のサーバーレンダリング/フェッチを避けることができます。


[Client Rendering](https://reactrouter.com/start/framework/custom#client-rendering), [Server Rendering](https://reactrouter.com/start/framework/custom#server-rendering) の詳細は公式ドキュメントを参照ください。



（出典）[Rendering Strategies](https://reactrouter.com/start/framework/rendering) / [Custom Framework](https://reactrouter.com/start/framework/custom)


## アップグレード

[React Router v6 から v7 へのアップグレード](https://reactrouter.com/upgrading/v6)

v6 → v7 のアップグレードでは、フューチャーフラグを有効にすれば変更点はありません。フィーチャーフラグを利用して一つずつ段階的に変更を適用しながら進めることができます。


[Remix v2 から React Router v7 へのアップグレード](https://reactrouter.com/upgrading/remix) 

割愛します。公式ドキュメントを参照してください。



# おわりに
React フレームワークを選択する上で Next.js や Remix は選択肢に上がると思います。筆者は既に React Router を使っているので v7 にバージョンアップすることで Remix の Web 標準に沿ったフレームワークを使い始める環境が整うので、まずは v7 にバージョンしてみたいと思いました。

（おまけ）
公式ドキュメントを読みながら調べていく中で日本語訳サイトを発見したので共有します。

https://react-router-docs-ja.techtalk.jp/

https://remix-docs-ja.techtalk.jp/