🎰

React Router v7 がプレリリースされた🎉

2024/10/13に公開

最初に

2024/10/4にReact Router v7がプレリリースされました。

https://reactrouter.com/dev/guides

今回はプレスリリースされた内容を紹介します。(以降React RouterをRRと表記します)

まず、RR v6 までは Reactでアプリを作成する上でのルーティング機能を提供するライブラリでした。
RR v7 では 同社のRemixというフルスタックフレームワークが(一旦)合流したことにより、React Router自体がルーティングライブラリからフルスタックフレームワークとなりました。

フルスタックフレームワークがルーティングライブラリと(一旦)統合された経緯については以下の記事をご覧ください。簡単にいうと両者の差がなくなってきたからです。

https://remix.run/blog/merging-remix-and-react-router

RR v7のはじめ方

何もない状態から!

RR v7の機能をすぐに試したい方は公式が提供している以下のコマンドで始めてください。

npx degit remix-run/react-router/templates/basic#dev my-app (my-app 部は任意です)

手動でパッケージを入れるところからやりたい方はこちらから始めてください。

  • ディレクトリ作成と必要なパッケージインストール
mkdir rr-v7
cd rr-v7
npm init -y
npm i react react-dom react-router@pre @react-router/node@pre @react-router/serve@pre
npm i -D vite @react-router/dev@pre 
typescriptの設定(不要な方はスキップしてください)
npm i -D typescript
touch tsconfig.json

tsconfig.jsonにコピペしてください(公式提供コマンドから確認できたtsconfigの設定です)

tsconfig.json
{
  "include": [
    "**/*.ts",
    "**/*.tsx",
    "**/.server/**/*.ts",
    "**/.server/**/*.tsx",
    "**/.client/**/*.ts",
    "**/.client/**/*.tsx",
    ".react-router/types/**/*"
  ],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "types": ["@react-router/node", "vite/client"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "target": "ES2022",
    "strict": true,
    "allowJs": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "rootDirs": [".", "./.react-router/types"],
    "plugins": [{ "name": "@react-router/dev" }]
  }
}

次にRRを始める上でのベースとなるファイルを作成します(上でtypescriptの設定をしていない方は拡張子をts → js, tsx → jsxにしてください)

mkdir app
touch app/routes/root.tsx
touch app/routes/home.tsx
touch app/routes.ts
touch vite.config.ts

ここから各ファイルに内容を記述していきます。まずはviteの設定からです
vite内にRR v7から提供されるRRのプラグインを書きます

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],
});
package.json
{
  "type": "module",
  "scripts": {
    "dev": "react-router dev",
    "build": "react-router build",
    "start": "react-router-serve ./build/server/index.js"
  }
}

コンポーネントの中身を書きます。なるべく最小にするために一旦下記にしましょう

app/routes/*.tsx
export default function Index() {
    return <div>/** ファイルの名前でも入れておきましょう */</div>
}

RR v6からの移行

試していないのでURLを載せておきます。
https://reactrouter.com/dev/guides/upgrading/v6#upgrade-to-v7

Remix v2 (vite) からの移行

「何もない状態から!」を見つつ該当箇所を変更してください。
Remix v2(vite)からの移行は、

  • RRの関連パッケージ導入
  • viteのプラグイン変更
  • RemixからimportしていたものをRRからimport
    が主な作業になりそうです。

ここまでできていれば npm run dev でローカルサーバーが立ち上がるはずです。
ここからは公開されている機能をピックアップして紹介します。↑でセットアップした構成で進めます。

RR v7での新機能

  • ルーティングについて
    • コンフィグベースルーティング
    • ファイルベースルーティング
  • データローディングの型安全の向上
  • レンダリングについて
    • CSR
    • SSR
    • StaticPreRendering

ルーティングについて

コンフィグベースのルーティング
RR v7では コンフィグベースのルーティングというものが導入されます。まずは形を見てみましょう。
構成は以下とします。(ここまでで作成していないファイルは作成してください)

my-project/
├── app/
│   ├── routes.ts
│   ├── routes/
│   │   ├── home.tsx
│   │   ├── about.tsx
│   │   ├── users/
│   │   │   ├── profile.tsx
│   │   │   └── settings.tsx
│   │   └── todos/
│   │       ├── list.tsx
│   │       └── detail.tsx
│   └── layouts/
│       └── base-layout.tsx
└── package.json(など)
app/routes.ts
import { route, index, layout, prefix, type RouteConfig } from "@react-router/dev/routes"
export const routes: RouteConfig = [
  // 以下のファイルはroot.tsx内の <Outlet /> を配置した箇所でレンダリングされます。
  index("./routes/home.tsx"),
  route("about", "./routes/about.tsx"),

  layout("layouts/base-layout.tsx", [
    // 以下の2ファイルはbase-layout.tsx内の <Outlet /> を配置した箇所でレンダリングされます。
    route("profile", "./routes/users/profile.tsx"),
    route("settings", "./routes/users/settings.tsx"),
  ]),

  ...prefix("todos", [
    // 以下の2ファイルはroot.tsx内の <Outlet /> を配置した箇所でレンダリングされます。
    index("./routes/todos/list.tsx"),
    route(":todoId", "./routes/todos/detail.tsx"),
  ]),
]; 

上記記述できたら設定したパスにアクセスしてみましょう。
http://localhost:5173/about にアクセスしてabout と画面に出れば成功です。

ルーティング設定した関数について触れておきます。
関数にはURLに一致するパターンと、そのURLで開くページを定義するルートモジュール(コンポーネント)のファイルパスを記述します。

もう少し書き方を見ていきましょう。

  • layout 関数は複数ファイルの枠となるものを定義します。第一引数には枠にしたいコンポーネントのファイルパス、第二引数にはレイアウトを対応させるコンポーネントのファイルパスを配列で記述します。
  • index 関数はindexを書いたネスト位置のルートパスを定義します。第一引数にコンポーネントのファイルパスを記述すればindexを書いた位置のルートパスに対応させられます。
  • route 関数は指定したパスを定義します。第一引数にパス名と、第二引数にパスに対応するコンポーネントのファイルパスを書きます。
  • prefix 関数は指定したパスを親とできます。第一引数に親としたいパス名、第二引数に親のパス名を使用したい設定(上の3つ)を書きます。

ファイルベースルーティング
Remix v2まで使っていた人はお馴染みのファイルベースルーティングも @react-route/fs-routes を使用することによって設定ができます。
ファイルベースルーティングには別途ライブラリをインストールします。

npm i @react-router/fs-routes@pre
app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatroutes } from "@react-router/fs-routes";

export const routes: RouteConfig = flatRoutes()

上記記述したらファイル名がパスに紐づいています。
about.tsx → /about
todos.$todoId.tsx → /todos/{todoId} $を先頭にしたtodoIdがコンポーネント内で取得できます。

設定をコンフィグベースに戻して以下を進めていきたいと思います。

型安全の向上

Remixではloader関数でGETメソッドアクセス時のサーバー側の処理を記述できます。(POSTはaction関数)

app/routes/about.tsx
import { ComponentProps } from "./+types.about";

export function loader() {
  return {
    title: "このサイトについて",
    description: "このサイトについての説明をします",
  }
}

export default function About({ loaderData }: ComponetProps) {
  return <div>{loaderData.title} - {loaderData.description}</div>
}

サーバーから返ってくる値をコンポーネント定義関数の引数より取得できます。
ここで注目なのが ComponetProps です。
RR v7では react-router typegen というコマンドで各パスに紐づけたコンポーネントの型が自動生成されます。一度実行し、その後ファイルの内容を変更すると勝手に型定義のファイルも変わるようです。

自動生成された型ファイル
+types.about.d.ts
// React Router generated types for route:
// ./routes/about.tsx

import * as T from "react-router/types"

export type Params = {}

type Route = typeof import("./about")

export type LoaderData = T.CreateLoaderData<Route>
export type ActionData = T.CreateActionData<Route>

export type LoaderArgs = T.CreateServerLoaderArgs<Params>
export type ClientLoaderArgs = T.CreateClientLoaderArgs<Params, Route>
export type ActionArgs = T.CreateServerActionArgs<Params>
export type ClientActionArgs = T.CreateClientActionArgs<Params, Route>

export type HydrateFallbackProps = T.CreateHydrateFallbackProps<Params>
export type ComponentProps = T.CreateComponentProps<Params, LoaderData, ActionData>
export type ErrorBoundaryProps = T.CreateErrorBoundaryProps<Params, LoaderData, ActionData>

この自動生成の型ファイルを使用しloader関数の引数, action関数の引数, コンポーネントが受け取るデータの型を充てられます。

Remixだとloaderからの値取得に型をつけるには以下のように useLoaderDataをインポートしていました。

import { useLoaderData } from "@remix-run/react";

export default function About() {
  const loaderData = useLoaderData<typeof loader>();
  return <div>{loaderData.title} - {loaderData.description}</div>
}

引数で取得できるようになったのとuseLoaderDataを使用せずに型が効くようになったのはいいですね。
また、Remixではリクエストしたパスパラメータをloaderの引数の型は

- import type { LoaderFunctionArgs } from "@remix-run/node";
+ import type { LoaderArgs } from "./+types.about";

- export function loader({params, request}: LoaderFunctionArgs) {}
+ export function loader({params, request}: LoaderArgs) {}

とすればOKです。(actionもErrorBoundaryも同様で、引数に ActionArgs, ErrorBoundaryProps を引数の型にすればOKです)

レンダリングについて

ここからは RR v7で使用できるレンダリングとその設定方法を見ていきます

  • CSR

RR v6でSPAを作成してきた方にとっては必須の設定になります。

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    reactRouter({
      ssr: false,
    }),
  ],
});

RR v6.4から導入されているData APIsのloader, actionを使用していて、RR v7でreactRouterブラグインを使用している方は loader → clientLoaderaction → clientAction の書き換えが必要です。

- export function loader() {}
- export function action() {}
+ export function clientLoader() {}
+ export function clientAction() {}
  • SSR

何も設定していない状態だとRR v7ではデフォルトでSSRになります。
SSRでもclientLoaderは使用でき、ユースケースとしてはブラウザ側のキャッシュのやり取りなどで使用されます。

SSRでclientLoaderを使用したいときには以下の設定を個々のコンポーネントに書きます

*.tsx
clientLoader.hydrate=true;

こちらはRRの Link コンポーネントで遷移した場合は clientLoader しか実行されません。(Link コンポーネントがクライアントサイドルーティングをするためです)

その場合もclientLoaderの引数に serverLoaderというのがあるので、そちらを実行すればサーバーにリクエストできます。

ごちゃごちゃ書いたのでまとめると。

SSRでclientLoaderを使いたい場合は clientLoaderhydrate=true の設定が必要。
Linkコンポーネント使用しての遷移時にサーバー側の処理をしたいときには clientLoader 内で serverLoader関数を使用する。
となります。

  • StaticPreRendering

ビルド時にHTMLを作成します。
StaticPreRenderingをするには以下の設定をします。

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    reactRouter({
      prerender: true, // いくつか種類がある!
    }),
  ],
});

prerenderの指定方法には 真偽値、配列、関数の3通りがあります。

まずここまでの routes.tsの構成から静的と動的パスを並べます

/ - 静的
/about - 静的
/profile - 静的
/settings - 静的
/todos - 静的
/todos/:todoId - 動的
  • 真偽値の場合

prerender: true にしたときに、上記リストの静的なパスのみプリレンダリングされます。

  • 配列

prerender: ["/about"] としたときにパス指定した /、と/aboutがプリレンダリングされます。

  • 関数

関数は引数がoptionalになっています。
引数からは静的なパスがリストとして取得できる関数が取得できます。

関数の場合も配列と同じく指定したパスがプリレンダリングされます。
配列との使い分けは、例えば 動的ルートである各todoを全て静的にするなら以下の形になります。

vite.tsconfig.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import { getTodos } from "apiClient";

export default defineConfig({
  plugins: [
    reactRouter({
      prerender: () => {
        const todos = getTodos();
        return todos.map(todo => todo.id);
      }
    }),
  ],
});

ということで

  • ルーティングについて
  • データローディングの型安全の向上について
  • レンダリングについて

を紹介しました。

RR v7に導入される機能で他には…

ほかにも、公式がアナウンスしているものとして

  • コード分割
  • データローディング
  • サーバーアクション
  • サーバーレンダリング
  • React Server Components

などがあります。

React Server Componentsは現時点の構想だと以下の形になるようです。

export async function loader() {
  return {
    products: <Products />,
    reviews: <Reviews />,
  };
}

export default function App({ data }) {
  return (
    <div>
      {data.products}
      {data.reviews}
    </div>
  );
}

RR v7でmiddlewareが入ってくるかと思ったのですが、Remixメンテナのsergiodxaさんのコメントを見るともう少し先になりそうです。(?)
https://github.com/remix-run/remix/discussions/7642#discussioncomment-10792203

↑にある通りgetLoadContext を使用するのがいいでしょう。それか万能アダプタのHono を前段に置きましょう。

最後に…

RRはメージャーバージョンのアップデートごとに大きな変化をしてきました。Remixもそれは言えることでRemix v1 → v2の変更時にも独自コンパイラからvite移行など大きな変化がありました。

現時点でRR v5を使用している方や、RR v6でも <Route>コンポーネントでルーティングを設定している人も少なくないと思うので、そういった方はRR v7の正式リリースを待ってコンフィグベースにするのか、ファイルベースにするのかを選べばよさそうです。

今回のプレリリースに対する感想としては、RR v7 のプレリリースするまで動向を追えてなかったので、単にインポート元を変えるのが最初の動きになると思っていました。
なのでコンフィグベースやPreRenderingが導入されるのはテンションが上がりました。

ということでまた進展があれば紹介したいと思います!では。

Discussion