React Router v7 さわってみた


# 1. この記事について


待望のReact Router v7のstatic版が登場したので
チュートリアルを一通りやってみます

https://reactrouter.com/start/framework/installation


# 2. React Router v7について


最近はSPAといえどルーティングしたいというのが一般的になっていて、SPA書くときにも素のReactで書くのではなくReact-RouterかRemix SPAモードかを選択するケースが増えているのではないかと思います。npx create-react-appが非推奨になったこともあってその傾向が助長されていて、2024年1月に公開された以下のスライドからもそんな雰囲気が伺えます
https://speakerdeck.com/nkzn/the-spas-chronicle-reaches-to-remix


2024年5月、React Routerがv7からRemixと統合されるというアナウンスが出て、Remix SPAモード勢が喜んだり？、React Router勢がまた書き方変わるのかと不平を並べたりしていました
https://remix.run/blog/merging-remix-and-react-router


そして、待望のReact Router v7 安定版初期リリースであるv7.0.1が2024年11月に公開されました。ちょうど新しいSPAを書く予定があったので、今回はこれで書こうとチュートリアル読みつつメモ書きしていくというのがこの記事です
https://remix.run/blog/react-router-v7


# 3. インストール


node.jsを入れた環境で以下を実行します


```react:terminal
npx create-react-router@latest my-app
```


開発サーバーを起動します


```react:terminal
cd my-app
npm i
npm run dev
```


開発サーバーを起動すると http://localhost:5173 に初期ページが待機します


<a href="http://localhost:5173">![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/826bb3f1-0b0f-bd79-2c81-bef240960c37.png)</a>


ディレクトリ構造はこんな感じ


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/57aca0e5-c70c-d17c-d43e-3ba6ae6c7ecc.png" width=25%>


root.tsxから始まって、routes.tsに書いているルーティング設定通りにコンポーネントを開くようになっています


# 4. Routing


ということで、まずはroutes.tsを読めるようにならないと何も分かりません
routes.tsにどう書けば良いのかやり方を見ていきます


## 4-1. Routingの手動設定


app/routes.tsでrouting方式の設定ができます


npx create-react-routerで生成されるroutes.tsは以下のようになっていて、routes/home.tsxをインデックスとするシングルページ構成を指示しています


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

export default [
  index("routes/home.tsx")
] satisfies RouteConfig;
```


公式ドキュメントに書かれているシンプル版が以下で、/some/pathに./some/file.tsxを表示するようにルーティングします。表示するページが固定されているならこれだけで問題ないですね


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

export default [
  route("some/path", "./some/file.tsx"),
  // pattern ^           ^ module file
] satisfies RouteConfig;
```


indexとrouteを組み合わせて色々足したのが以下


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

export default [
  // renders into the root.tsx Outlet at /
  index("./home.tsx"),
  route("dashboard", "./dashboard.tsx", [
    // renders into the dashboard.tsx Outlet at /dashboard
    index("./dashboard-home.tsx"),
    route("settings", "./dashboard-settings.tsx"),
  ]),
] satisfies RouteConfig;
```


## 4-2. Nested Routes


共通のナビゲーションバーやサイドバーを表示するような場合に、共通部分を分けて書ける仕組みがNested Routesです。以下では./dashboard.tsxに共通部分を書いて、./home.tsx, ./settings.tsxがそれぞれのURLに表示する内容を書いていて、URLで見ると/dashboard/に./dashboard.tsx＋./home.tsx、/dashboard/settings/に./dashboard.tsx＋./settings.tsxが表示されるわけです


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

export default [
  // parent route
  route("dashboard", "./dashboard.tsx", [
    // child routes
    index("./home.tsx"),
    route("settings", "./settings.tsx"),
  ]),
] satisfies RouteConfig;
```


共通部分になる./dashboard.tsxには個別部分をOutlet要素として書くことで自動的に置き換えて表示してくれる仕組みです。これはRemixと同様のスタイルですね


```react:app/dashboard.tsx
import { Outlet } from "react-router";

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* will either be home.tsx or settings.tsx */}
      <Outlet />
    </div>
  );
}
```


## 4-3. Layout Routes


routeの代わりにlayoutを使うとNestしつつURLの移動はないというのが書けます


以下の場合は
/ に./dashboard.tsx＋./home.tsx が表示
/settings/に./dashboard.tsx＋./settings.tsxが表示
となります


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

export default [
  // parent route
  layout("./dashboard.tsx", [
    // child routes
    index("./home.tsx"),
    route("settings", "./settings.tsx"),
  ]),
] satisfies RouteConfig;
```


## 4-4. Route Prefixes


prefixを使うと親ファイルなしでNestできます


/projects/ に./projects/home.tsx
/projects/:pid に./projects/project.tsx
/projects/:pid/edit に./projects/edit-project.tsx
を表示します


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

export default [
  layout("./marketing/layout.tsx", [
    index("./marketing/home.tsx"),
    route("contact", "./marketing/contact.tsx"),
  ]),
  ...prefix("projects", [
    index("./projects/home.tsx"),
    layout("./projects/project-layout.tsx", [
      route(":pid", "./projects/project.tsx"),
      route(":pid/edit", "./projects/edit-project.tsx"),
    ]),
  ]),
] satisfies RouteConfig;
```


## 4-5. Dynamic Segment


### 4-5-1. 単独


動的なルーティングをしたい場合は以下のように「:」を頭に付けます
例えば以下のように書くと /teams/123/ が team.tsxに引数teamId=123としてルーティングされます

```react:
route("teams/:teamId", "./team.tsx"),
```


ページコンポーネント側はデータ受け取りを担当するloaderさんにparamsとして受け取るように書いていて、params.teamIdとしてコンポーネント内で使えるように書きます


```react:./team.tsx
import type { Route } from "./+types/team";

export async function loader({ params }: Route.LoaderArgs) {
  //                           ^? { teamId: string }
}

export default function Component({
  params,
}: Route.ComponentProps) {
  params.teamId;
  //        ^ string
}
```


### 4-5-2. 複数


複数の動的なセグメントを入れることもできます


```react:
route("c/:categoryId/p/:productId", "./product.tsx"),
```


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

async function loader({ params }: LoaderArgs) {
  //                    ^? { categoryId: string; productId: string }
}
```


## 4-6. Optional Segments


セグメントがあってもなくても同じファイルにルーティングする場合「?」をつけてやります


例えば以下だと動的セグメントである /:lang/ があってもなくても ./categories.tsx にルーティングされます


```react:
route(":lang?/categories", "./categories.tsx"),
```


動的でないセグメントもOptionalにできます


```react:
route("users/:userId/edit?", "./user.tsx");
```


## 4-7. Splats


「*」で指定するとそこに何がきても同じファイルにルーティングできます


```react:
route("files/*", "./files.tsx"),
```


コンポーネント側はparams["*"]で受け取れます


```react:./files.tsx
export async function loader({ params }: Route.LoaderArgs) {
  // params["*"] will contain the remaining URL after files/
}
```


params["*"] と書くのがちょっとめんどい、という方は適当な名前に代入して扱えます。例えば以下のように書くと const splat = params["*"] として扱うことができます


```react:
const { "*": splat } = params;
```


## 4-8. Component Routes


なんとTSXタグとしてルーティングが書けるようです


例えば下のように書くと
/ に &lt;StepOne /&gt;
/step-2/ に &lt;StepTwo /&gt;
/step-3/ に &lt;StepThree /&gt;
が表示されます


```react:
import { Routes, Route } from "react-router";

function Wizard() {
  return (
    <div>
      <h1>Some Wizard with Steps</h1>
      <Routes>
        <Route index element={<StepOne />} />
        <Route path="step-2" element={<StepTwo />} />
        <Route path="step-3" element={<StepThree />}>
      </Routes>
    </div>
  );
}
```


混乱しそう
慣れるのだろうか？


なお、この書き方だとloader, actionなどのRoute Moduleの機能が使えないようです


# 5. Route Module


以下の./team.tsxのように routes.ts で割り当てているファイルをRoute Moduleというようです


```react:/routes.ts
route("teams/:teamId", "./team.tsx"),
//           route module ^^^^^^^^
```


Route Moduleには以下のような機能があるようです


・automatic code-splitting
・data loading
・actions
・revalidation
・error boundaries
・and more


## 5-1. 基本の書き方


Route Moduleと言ってますが、Reactのコンポーネントと同じ書き方が基本型で、ここにReact Router独自の機能を追加したものをRoute Moduleと呼んでいるようです


```react:/routes/hoge.tsx
export default function MyRoute() {
  return (
    <div>
      <h1>Look ma!</h1>
      <p>
        I'm still using React Router after like 10 years.
      </p>
    </div>
  );
}
```


## 5-2. ErrorBoundary 


ルーティング処理でエラーが出た時にエラーハンドリングすることができます。データフェッチなどでエラーが出た場合に専用の表示を出すような使い方が想定されているようです


```react:routes/hoge.tsx
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>;
  }
}
```


## 5-3. HydrateFallback 


データフェッチなどの処理が入るページで、処理が完了するまでの間にローディングページなどを表示することができます


```react:routes/hoge.tsx
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} />;
}
```


## 5-4. HTTP headers


HTTPヘッダを以下のように書くことができます


```react:routes/hoge.tsx
export function headers() {
  return {
    "X-Stretchy-Pants": "its for fun",
    "Cache-Control": "max-age=300, s-maxage=3600",
  };
}
```


## 5-5. handle


パンくずリストのようなルート経路を使った表示をつくりたい場合、以下のように各コンポーネントでexport constしておいて、useMatchesで受け取ることができます



```react:routes/index.tsx
export const handle = {
  breadcrumb: "Home",
};

export default function Index() {
  return <h1>Home Page</h1>;
}
```


```react:routes/routes/about.tsx
export const handle = {
  breadcrumb: "About",
};

export default function About() {
  return <h1>About Page</h1>;
}
```


```react:routes/routes/contact.tsx
export const handle = {
  breadcrumb: "Contact",
};

export default function Contact() {
  return <h1>Contact Page</h1>;
}
```


```react:breadcrumbs.tsx
import { useMatches, Link } from "@remix-run/react";

export default function Breadcrumbs() {
  const matches = useMatches();

  // ルートのhandleでパンくずリスト情報が定義されているもののみ取得
  const breadcrumbs = matches
    .map((match) => {
      if (match.handle && match.handle.breadcrumb) {
        return {
          breadcrumb: match.handle.breadcrumb,
          pathname: match.pathname, // ルートのパス
        };
      }
      return null;
    })
    .filter(Boolean); // nullを取り除く

  return (
    <nav aria-label="Breadcrumb">
      <ul style={{ listStyle: "none", display: "flex", gap: "8px", padding: 0 }}>
        {breadcrumbs.map((crumb, index) => (
          <li key={crumb.pathname}>
            <Link to={crumb.pathname}>
              {crumb.breadcrumb}
            </Link>
            {index < breadcrumbs.length - 1 && <span> / </span>}
          </li>
        ))}
      </ul>
    </nav>
  );
}
```


## 5-6. links


linkタグに書く内容をまとめて定義しておいて、各ページに共通して追加することができます


```react:links.tsx
export function links() {
  return [
    {
      rel: "icon",
      href: "/favicon.png",
      type: "image/png",
    },
    {
      rel: "stylesheet",
      href: "https://example.com/some/styles.css",
    },
    {
      rel: "preload",
      href: "/images/banner.jpg",
      as: "image",
    },
  ];
}
```


以下のように書くとLinks要素のところに


```react:hoge.tsx
import { Links } from "react-router";

export default function Root() {
  return (
    <html>
      <head>
        <Links />
      </head>

      <body />
    </html>
  );
}
```


## 5-7. meta


metaタグの内容もまとめて書いておけます


```react:
export function meta() {
  return [
    { title: "Very cool app" },
    {
      property: "og:title",
      content: "Very cool app",
    },
    {
      name: "description",
      content: "This app is the best",
    },
  ];
}
```



```react:hoge.tsx
import { Meta } from "react-router";

export default function Root() {
  return (
    <html>
      <head>
        <Meta />
      </head>

      <body />
    </html>
  );
}
```


## 5-8. shouldRevalidate


routes.tsにshouldRevalidate関数を書いておくとrevalidateするかどうか指示することが出来ます。ユーザーがフォーム送信するなどしてデータが更新されたときに、値を再評価してレンダリングしなおすrevalidateがデフォルトではすべてのページで有効になっていますが、その必要がないページはrevalidate無効としておくことでパフォーマンスを改善できます


```react:routes.ts
import type { Route } from "./+types/my-route";

export function shouldRevalidate(
  arg: Route.ShouldRevalidateArg
) {
  return true;
}
```


shouldRevalidateの引数には以下のような値を受け取れるので、これを使ってrevalidateするかどうか条件分岐することができます


|key  |内容  |
|---|---|
|currentUrl  |現在のURL  |
|nextUrl  |ナビゲーション後のURL  |
|defaultShouldRevalidate  |React Routerがデフォルトで適用するshouldRevalidateロジックの結果 |
|formMethod  | 現在のフォーム送信のHTTPメソッド（"POST", "PUT"など） |
|formData  |フォーム送信に関連するデータ（FormDataオブジェクト）  |
|currentParams  |現在のルートパラメータ  |
|nextParams  |ナビゲーション後のルートパラメータ  |


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

// shouldRevalidateの定義例
function myShouldRevalidate({ currentUrl, nextUrl }: { currentUrl: URL; nextUrl: URL }) {
  return currentUrl.pathname !== nextUrl.pathname; // パスが変更された場合のみ再取得
}

export default [
  // parent route
  layout("./dashboard.tsx", [
    // child routes
    index("./home.tsx", { shouldRevalidate: myShouldRevalidate }), // ホームルートに追加
    route("settings", "./settings.tsx", { shouldRevalidate: myShouldRevalidate }), // 設定ルートに追加
  ]),
] satisfies RouteConfig;
```


# 6. loader, action


## 6-1. コンセプトとか経緯とか


2020年11月にRemixがloader, actionを提案してフロントエンドを書くときにいちいちデータ更新を考える必要をなくそう、バックエンドのデータを更新したらフロントエンドも更新されるように勝手になる仕組みを作ろう、というコンセプトを提唱しました
https://remix.run/docs/en/main/discussion/data-flow
https://remix.run/blog/remix-data-flow
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/cb7a43c2-050f-2d9c-10ed-3f598c0ed1cf.png" width=40%>


SSRとCSRが混在している現在のフロントエンドでこのコンセプトがすごく良いというのは僕にも分かりますし、実際界隈にも受け入れられたようで2022年9月のReact Routerもv6.4からloader, actionを導入、2024年春にRemixとのマージをブログで発表、2024年11月v7がリリースされています
https://remix.run/blog/merging-remix-and-react-router


React Router v6.4について書かれたブログを見ると少なからず驚きをもって迎えられている雰囲気が伝わってきます
https://zenn.dev/monicle/articles/react-router-v6-4?utm_source=chatgpt.com


そうして導入されてきたloader, actionはReact Router v7ではどうなっているでしょうか


## 6-2. Rendering Strategies


SSRを有効にするか、どのページをprerenderするかまとめて設定しておくことができます


### 6-2-1. Server Side Rendering


デフォルトだとSSRが有効になっています


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

export default {
  ssr: true,
} satisfies Config;
```


### 6-2-2. Client Side Rendering


SPAにしたいときは無効にします


```react:react-router.config.ts
  ssr: false,
```



### 6-2-3. Static Pre-rendering


prerenderに指定しておくことでビルド時に静的ファイルを生成してくれるようです


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

export default {
  // return a list of URLs to prerender at build time
  async prerender() {
    return ["/", "/about", "/contact"];
  },
} satisfies Config;
```


公式ドキュメントにpre-renderingのページがあり、ここを読むと他の書き方も紹介されています
https://reactrouter.com/how-to/pre-rendering#pre-rendering


以下のように3つの書き方がまとめて紹介されていますが、通常はどれか1つを選択するのではないかと思います


prerender: trueは静的なrouteを全部pre-renderするけど動的なrouteは対象外
prerender: ["/", "/blog"]は指定のpathのrouteをpre-render
async prerender()はCMSなどのフェッチデータなどから動的にrouteを指定できるようです


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

export default {
  // all static route paths
  // (no dynamic segments like "/post/:slug")
  prerender: true,

  // any url
  prerender: ["/", "/blog", "/blog/popular-post"],

  // async function for dependencies like a CMS
  async prerender({ getStaticPaths }) {
    let posts = await fakeGetPostsFromCMS();
    return ["/", "/blog"].concat(
      posts.map((post) => post.href)
    );
  },
} satisfies Config;
```


動的に指定できるってことはSSGも出来るのかな？


## 6-3. loader


React Router v7ではrouting, loader, actionが一体となって動作します
データの受け取りをすべてloader内でやると適切なタイミングでrevalidateしてくれます


loaderとclientLoaderの2種類が用意されていて、SSRやstatic pre-renderingのときはloader、CSRのときはclientLoaderを使います


### 6-3-1. Client Data Loading


clientLoader関数内でデータフェッチしてreturnしてやると、Route Module側でloaderDataとして受け取れます


```react:
// 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>
  );
}
```


clientLoaderはreact-router.config.tsxでssr: falseにしていても使う事ができますので、SPAをビルドして使うときもclient側フェッチはこちらで書くことになります
https://reactrouter.com/how-to/spa


### 6-3-2. Server Data Loading


こちらも同様にデータフェッチしてreturnしてやるとloaderDataとして受け取れます
サーバーサイド側はDBから取得すると思いますので、prismaなどを使って取ってくる感じでしょうか


```react:
// 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>
  );
}
```


### 6-3-3. Static Data Loading


プリレンダリングするときは普通にloaderで書いておいて、react-router.config.tsxでprerenderするだけです


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

export async function loader({ params }: Route.LoaderArgs) {
  let product = await getProductFromCSVFile(params.pid);
  return product;
}

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


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

export default {
  async prerender() {
    let products = await readProductsFromCSVFile();
    return products.map(
      (product) => `/products/${product.id}`
    );
  },
} satisfies Config;
```


### 6-3-4. 両方使う


なんと、loaderとclientLoaderの両方を同時に使う事ができます
受け取り側はloaderDataでまとめて受け取り


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

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

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

export default function Product({
  loaderData,
}: Route.ComponentProps) {
  const { name, description } = loaderData;

  return (
    <div>
      <h1>{name}</h1>
      <p>{description}</p>
    </div>
  );
}
```


素晴らしい
早くこれで書きたい


## 6-4. action


バックエンドへの書き込みを担当するのがactionです
データの更新処理をactionに書いておいて、actionをトリガーするイベントが発生するとrevalidateしてくれます


### 6-4-1. clientAction


client側で実行されるべきアクションはclientActionに書きます
以下の場合は、Form内のsubmitボタンがactionをトリガーしてくれます


```react:
// 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>
  );
}
```


### 6-4-2. action


actionも書き方は同じですがサーバーサイドでのactionを想定しているので、公式ドキュメントの例でもDBへの書き込み処理を記述しています


```react:
// 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>
  );
}
```


### 6-4-3. actionをトリガーするイベント


#### HTTPリクエスト


先ほどの例で書かれていたFormのボタン操作のように、HTTPリクエストをトリガーとする場合です。action側でPOSTリクエストを引数requestとして受け取って処理します


```react:
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


useSubmitによりactionをトリガーできます
任意のイベントから実行できるので、これで書くことが多そうです


```react:
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


FormやuseSubmitを使うとブラウザのナビゲーション履歴が残りますが、useFetcherを使うと履歴が残らなくできます


```react:Formを置き換える場合
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>
  );
}
```


```react:useSubmitを置き換える場合
fetcher.submit(
  { title: "New Title" },
  { action: "/update-task/123", method: "post" }
);
```


# 7. Navigation


React Router v7のルーティングに沿ってページ遷移する為の仕組みとして&lt;Link&gt;, &lt;NavLink&gt;, &lt;Form&gt;, redirect, and useNavigateが準備されています


## 7-1. NavLink


ルーティングのPathを指定してリンクすることができます


アクティブやペンディングなどの状態を反映して表示などを変えたい場合はNavLink
使わない場合はLinkを使います


```react:hoge.tsx
import { NavLink } from "react-router";

export function MyAppNav() {
  return (
    <nav>
      <NavLink to="/" end>
        Home
      </NavLink>
      <NavLink to="/trending" end>
        Trending Concerts
      </NavLink>
      <NavLink to="/concerts">All Concerts</NavLink>
      <NavLink to="/account">Account</NavLink>
    </nav>
  );
}
```


NavLinkにCSSを適用する場合、以下のように書くだけで上手くやってくれます


```react:hoge.css
a.active {
  color: red;
}

a.pending {
  animate: pulse 1s infinite;
}

a.transitioning {
  /* css transition is running */
}
```


普通の要素と同様、stateを使ってclassNameなどを変更することができます



```react:stateに応じてclassNameを変更する
// className
<NavLink
  to="/messages"
  className={({ isActive, isPending, isTransitioning }) =>
    [
      isPending ? "pending" : "",
      isActive ? "active" : "",
      isTransitioning ? "transitioning" : "",
    ].join(" ")
  }
>
  Messages
</NavLink>
```





```react:stateに応じてstyleを変更する
// style
<NavLink
  to="/messages"
  style={({ isActive, isPending, isTransitioning }) => {
    return {
      fontWeight: isActive ? "bold" : "",
      color: isPending ? "red" : "black",
      viewTransitionName: isTransitioning ? "slide" : "",
    };
  }}
>
  Messages
</NavLink>
```



```react:stateに応じて子要素のclassNameを変更
// children
<NavLink to="/tasks">
  {({ isActive, isPending, isTransitioning }) => (
    <span className={isActive ? "active" : ""}>Tasks</span>
  )}
</NavLink>
```


## 7-2. Link


アクティブやペンディングなどの状態を使わない場合はLinkを使います


```react:
import { Link } from "react-router";

export function LoggedOutMessage() {
  return (
    <p>
      You've been logged out.{" "}
      <Link to="/login">Login again</Link>
    </p>
  );
}
```


## 7-3. Form


Formを使ってナビゲートすることもできます


```react:
<Form action="/search">
  <input type="text" name="q" />
</Form>
```


上の場合に例えば journey と入力すると、以下のようなPathへのナビゲートを要求します


```react:
/search?q=journey
```


## 7-4. redirect


loaderやactionの中でリダイレクトすることができるようです


```react:
import { redirect } from "react-router";

export async function loader({ request }) {
  let user = await getUser(request);
  if (!user) {
    return redirect("/login");
  }
  return { userName: user.name };
}
```


```react:
import { redirect } from "react-router";

export async function action({ request }) {
  let formData = await request.formData();
  let project = await createProject(formData);
  return redirect(`/projects/${project.id}`);
}
```


## 7-5. useNavigate


適当なイベントからuseNavigateを使ってナビゲートできます


```react:
import { useNavigate } from "react-router";

export function useLogoutAfterInactivity() {
  let navigate = useNavigate();

  useFakeInactivityHook(() => {
    navigate("/logout");
  });
}
```


# 8. Pending UI


新しいrouteに移動するとき、actionをトリガーするユーザー入力があったときには画面はすぐ反応させたいので、待機ページを表示するか何かしら最適化した表示を出したいですが、手動でやるのはわりと面倒だったり難しかったりしたと思います。React Router v7ではRouting, loader, actionが統合的に組み込まれたことでこれを解決し、待機ページ表示を指定するだけで上手に表示してくれるようになったようです


## 8-1. Global Pending Navigation


Nesting Routeの親側コンポーネントで以下のように書いておくと、子コンポーネントに共通して読み込み中の表示を適用できます。以下のGlobalSpinner要素はデフォルトで存在するコンポーネントではないので、自分で何かスピナー的な要素を書いて配置する想定のサンプルコードだと思われます


```react:
import { useNavigation } from "react-router";

export default function Root() {
  const navigation = useNavigation();
  const isNavigating = Boolean(navigation.location);

  return (
    <html>
      <body>
        {isNavigating && <GlobalSpinner />}
        <Outlet />
      </body>
    </html>
  );
}
```


## 7-2. Local Pending Navigation


NavLink要素以下ではisPendingとして遷移中かどうかを取得できます。isPending === trueであるときに、遷移中表示が出るように書いておけば、遷移し終わったらページ自体が入れ替わりますので自然な待機表示が実現します


```react:
import { NavLink } from "react-router";

function Navbar() {
  return (
    <nav>
      <NavLink to="/home">
        {({ isPending }) => (
          <span>Home {isPending && <Spinner />}</span>
        )}
      </NavLink>
      <NavLink
        to="/about"
        style={({ isPending }) => ({
          color: isPending ? "gray" : "black",
        })}
      >
        About
      </NavLink>
    </nav>
  );
}
```


## 7-3. Pending Form Submission


Formやfetcher.Formも待機表示が書けます


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

function NewProjectForm() {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post">
      <input type="text" name="title" />
      <button type="submit">
        {fetcher.state !== "idle"
          ? "Submitting..."
          : "Submit"}
      </button>
    </fetcher.Form>
  );
}
```


```react:
import { useNavigation, Form } from "react-router";

function NewProjectForm() {
  const navigation = useNavigation();

  return (
    <Form method="post" action="/projects/new">
      <input type="text" name="title" />
      <button type="submit">
        {navigation.formAction === "/projects/new"
          ? "Submitting..."
          : "Submit"}
      </button>
    </Form>
  );
}
```


## 7-4. Optimistic UI


待機中表示と同様にOptimistic UIも書くことができます
以下では開始して完了するまでの表示と完了後の表示を指定しています


```react:
function Task({ task }) {
  const fetcher = useFetcher();

  let isComplete = task.status === "complete";
  if (fetcher.formData) {
    isComplete = fetcher.formData.get("status");
  }

  return (
    <div>
      <div>{task.title}</div>
      <fetcher.Form method="post">
        <button
          name="status"
          value={isComplete ? "incomplete" : "complete"}
        >
          {isComplete ? "Mark Incomplete" : "Mark Complete"}
        </button>
      </fetcher.Form>
    </div>
  );
}
```


## 8. Testing


actionでエラーが発生した場合にキャッチしてコンポーネントに表示できます


以下ではFormをsubmitしたときにaction内でエラーが出た場合にactionからreturnされたエラー表示を画面に出して、何がまずかったかユーザーに表示しようとしています


```react:
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>
  );
}
```


この動作をテストする為にRoute Moduleのスタブを作ってテストするためのcreateRoutesStub関数が用意されています。これを使うと、単体のRoute Moduleのスタブを作って値を与えて挙動テストを実行することができます


```react:
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)
  );
});
```


# まとめ


まだほとんど書けていませんが、RemixとReact Router v6に良いところ取りをしたような書き方になっているような印象です。SPAでもloader, actionが使えるようになったり、エラーハンドリングが洗練されたりと全体にパワーアップしているようですので早く書いてみたいところです

レッツトライ