7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【脱react-router-dom】React で 超簡単 File-based ルーティング!!!【TanStack Router】

Last updated at Posted at 2024-10-12

File-based ルーティングの開発体験を紹介

File-based ルーティングの世界では、こんな感じの開発が行えます。

Animation.gif

少しでも興味をもってもらえたでしょうか?

この記事では以下の構成でFile-based ルーティングに入門します

① File-based ルーティングって?
② 実際にReactでFile-based ルーティングしてみる!~with TanStack Router~
③ TanStack Router をvitestでテストして、仕組みを理解する(別記事)

やっていきます!

① File-based ルーティングって?

File-based ルーティングについて、少し深掘りしてみます!!

File-based ルーティングとは、jsxファイルのルーティングを、コードでの設定ではなく、
物理的なディレクトリの位置で行おう!という考え方です。

コードベースのルーティングとファイルベースのルーティングを比較して、どんなものかイメージしてみます。

コードベースのルーティング(React-router-dom)

route.tsx
    <Router>
      <div>
        <Navbar />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/blog" element={<BlogIndex />} />
          <Route path="/blog/:id" element={<BlogPost />} />
        </Routes>
      </div>
    </Router>

コードベースルーティングのサンプルです。
なじみのある人も多いのではないでしょうか。

コードベースルーティングは、Reactコンポーネントをpathに紐づけてコード上で設定する方式です。

メリットはいろいろありますが、個人的には以下が特に好みです

  • すべてのルートが1つのファイルで管理可能
  • ルーティングの全体像が把握しやすい

ファイルベースのルーティング

routes/
├── index.tsx           // → /
├── about.tsx           // → /about
└── blog/
    ├── index.tsx       // → /blog
    └── $id/
         └── index.tsx  // → /blog/:id (動的ルーティング)

今回入門するファイルベースルーティングのサンプルです

FWまたはライブラリを使用/インストールするだけで基本的には設定が完了します。

設定ファイルなどの細かな設定・更新は不要で、
コンポーネントファイルを作成すると、そのファイル名やディレクトリが自動的にpathに設定されます。

routes/index.tsx/ でアクセス可能
routes/about.tsx/about でアクセス可能
routes/blog/index.tsx/blog でアクセス可能

メリットはいろいろありますが、個人的には以下が特に好みです

  • プロジェクト構造が視覚的に理解しやすい
  • 設定ファイルの更新が不要

今回はこのファイルベースルーティングを TanStack Router というライブラリでReactに実装していきます!!!!!

② 実際にReactでFile-based ルーティングしてみる!~with TanStack Router~

テンションを上げるために冒頭のgifを再掲します

Animation.gif

routes/test/sample.tsx/test/sample でアクセス可能になっていることが確認できます。

良い感じですね

やっていきましょう!

今回はこんな感じでTanStack Routerを触っていきます

  1. vite + React開発環境の作成
  2. tanstack routerをインストールする
  3. tanstack routerをセットアップする
  4. tanstack routerを使ってFile-based ルーティング!!!
  5. tanstack routerの凄さ・ありがたさに少し触れる

vite + React開発環境の作成

tanstack routerはvite環境に対応しているため、素直にviteを使います。

npm create vite@latest my-tanStack-app -- --template react-ts

ドキュメントを参考にプロジェクトを作成します。

> npm create vite@latest my-tanStack-app -- --template react-ts
 Package name: ... my-tanstack-app

Scaffolding project in C:\work\react\my-tanStack-app...

Done. Now run:

  cd my-tanStack-app
  npm install
  npm run dev

作成が完了しました。

  cd my-tanStack-app
  npm install

依存パッケージをインストールします。

> npm install

added 192 packages, and audited 193 packages in 49s

41 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

開発サーバーを起動して、動作確認します

npm run dev
> npm run dev

> my-tanstack-app@0.0.0 dev
> vite


  VITE v5.4.8  ready in 1209 ms

    Local:   http://localhost:5173/
    Network: use --host to expose
    press h + enter to show help

http://localhost:5173/ にアクセスします

キャプチャ.PNG

良い感じですね

では早速File-based ルーティングに入門しましょう!!

tanstack routerをインストールする

ライブラリ本体をインストールする

ドキュメントを参考にライブラリをインストールしていきます。

npm add -D @tanstack/router-plugin @tanstack/router-devtools @tanstack/react-router

※ドキュメントには記載がないですが、@tanstack/react-router ライブラリも動作に必要なのでインストールします

vite.config.ts にtanstack router用のpluginを追加する

viteではpluginという機能を用いて、ビルド時・ビルド中にファイル生成やコンパイル処理を行うことができます。

tanstack routerはこの仕組みを利用して、動的にファイルの生成や書き換えを行います。
プラグインを有効化しておくと、冒頭のgifのように、自動的にルーティングしてくれるので、有効化します。

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

// https://vitejs.dev/config/
export default defineConfig({
-  plugins: [react()],
+  plugins: [
+    TanStackRouterVite(),
+    react()
+  ],
})

pluginsをインポートして、追加する感じですね

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    TanStackRouterVite(),
    react()
  ],
})

完成形も載せておきます

routesディレクトリ、routes/__root.tsxを準備する

デフォルトでは、tanstack router は src/routes 配下のファイルを自動的にルーティングしてくれます
まずは src/routes を作成します。

mkdir src/routes

また、 src/routes 配下には、 __root.tsx というtsxが必要です。
このtsxは、routes配下の全てのルートで適用されるtsxです。
ヘッダーやフッターなどを記載しておくと便利ですね。

まずは作成してみます

touch src/routes/__root.tsx

中身はからっぽで大丈夫です。

上述した通り、tanstack routerはpluginsを利用して、動的にファイルの生成や書き換えを行います
__root.tsxの中身も初期化してくれるので、 npm run dev してプラグインを実行します。

npm run dev
> npm run dev

> my-tanstack-app@0.0.0 dev
> vite


♻️  Generating routes...
🟡 Creating C:\work\react\my-tanStack-app\src\routes\__root.tsx
🟡 Updating C:\work\react\my-tanStack-app\src\routeTree.gen.ts
 Processed routes in 432ms

  VITE v5.4.8  ready in 3055 ms

    Local:   http://localhost:5173/
    Network: use --host to expose
    press h + enter to show help

だーっとログが出て、ローカルサーバーが起動しました。

tanstack router pluginのログに注目してみます

♻️  Generating routes...
🟡 Creating C:\work\react\my-tanStack-app\src\routes\__root.tsx
🟡 Updating C:\work\react\my-tanStack-app\src\routeTree.gen.ts
 Processed routes in 432ms

🟡 Creating C:\work\react\my-tanStack-app\src\routes__root.tsx

__root.tsx の初期化をおこなってくれてます。中身を見てみます

src\routes__root.tsx
import * as React from 'react'
import { Outlet, createRootRoute } from '@tanstack/react-router'

export const Route = createRootRoute({
  component: () => (
    <React.Fragment>
      <div>Hello "__root"!</div>
      <Outlet />
    </React.Fragment>
  ),
})

Outletコンポーネントは子供コンポーネントをレンダリングしてくれるコンポーネントです(超意訳)

どのルートでも共通で表示したいもの、実行したい処理を記載することができます。
今回は見出しのみ表示させておきます

import * as React from 'react'
import { Outlet, createRootRoute } from '@tanstack/react-router'

export const Route = createRootRoute({
  component: () => (
    <React.Fragment>
-      <div>Hello "__root"!</div>
+     <h1>TanStack Router やってみた</h1>
      <Outlet />
    </React.Fragment>
  ),
})

共通のヘッダーやフッターを書きたい場合、
<Header /><Outlet /><Footer /> とするとよさそうですね

src\routes__root.tsx
import * as React from 'react'
import { Outlet, createRootRoute } from '@tanstack/react-router'

export const Route = createRootRoute({
    component: () => (
        <React.Fragment>
            <h1>TanStack Router やってみた</h1>
            <Outlet />
        </React.Fragment>
    ),
})

完成形です。

まだ、準備が必要なので準備します。

また、以下のログもとても大事なのですが、今解説するとややこしいので、細かい解説はテストコードを書く時に記載するので割愛します!

🟡 Updating C:\work\react\my-tanStack-app\src\routeTree.gen.ts

src/main.tsx を編集する

main.tsxで、tanstack routerをReactアプリ内部で使用するようコードを修正します

ここもとても大事なのですが、細かい解説はテストコードを書く時に記載するので割愛します!
とりあえずコピペで大丈夫です

src/main.tsx
import React, { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'

// Import the generated route tree
import { routeTree } from './routeTree.gen'

// Create a new router instance
const router = createRouter({ routeTree })

// Register the router instance for type safety
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

// Render the app
const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement)
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>,
  )
}

main.tsxを保存すると、viteがファイルの変更を検知し、
自動でpluginを実行してくれます。以下のログが表示されれば幸せです

10:41:01 [vite] page reload src/main.tsx
10:41:02 [vite]  new dependencies optimized: @tanstack/react-router
10:41:02 [vite]  optimized dependencies changed. reloading

10:41:02 [vite] ✨ new dependencies optimized: @tanstack/react-router
10:41:02 [vite] ✨ optimized dependencies changed. reloading

なんか成功っぽいログが出てます。いいですね

  1. [✓] vite + React開発環境の作成
  2. [✓] tanstack routerをインストールする
  3. [✓] tanstack routerをセットアップする
  4. [] tanstack routerを使ってFile-based ルーティング!!!
  5. [] tanstack routerの凄さ・ありがたさに少し触れる

ここまで長かった・・・

tanstack routerを使ってFile-based ルーティング!!!

起動している http://localhost:5173/を確認します。

cap1.PNG

__root.tsx で設定した見出しが表示されていますね!
ただ、 Not Found の表示も・・・

ただ、まだ何もファイルを配置していないので、ファイルベースルーティング的には正しい挙動ですね。

色々ファイルを置いて試していきます。

ここではTanStack Routerの主要なルーティングルールのうち、3つを確認します

ルール ① index.tsxはディレクトリパスにルーティングされる

routes/index.tsx を作成します。
routesはrootディレクトリなので、このindex.tsxは「/」で表示されるはずです。

touch src/routes/index.tsx

ファイルを作成します
すると、再度viteがファイルの変更を検知し、tanstack routerのpluginを実行します

♻️  Regenerating routes...
🟡 Updating C:\work\react\my-tanStack-app\src\routes\index.tsx
🟡 Updating C:\work\react\my-tanStack-app\src\routeTree.gen.ts
 Processed route in 139ms
10:50:11 [vite] page reload src/routeTree.gen.ts

それっぽいログが出ました!

🟡 Updating C:\work\react\my-tanStack-app\src\routes\index.tsx
🟡 Updating C:\work\react\my-tanStack-app\src\routeTree.gen.ts

🟡 Updating C:\work\react\my-tanStack-app\src\routes\index.tsx

どんな感じか見に行きましょう

src\routes\index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: () => <div>Hello /!</div>,
})

色々記載されてますね。大事なところだけ確認します

大事なところ①:ルートの作成

export const Route = createFileRoute('/')({

自動生成でルートへの登録が行われている部分です
いや、結局コードベースで設定しとんかーい、というツッコミを感じる方もいるかもしれませんが、
この記述はpluginが行ってくれます。また、引数を勝手に変えると怒られます。

cap2.PNG

型で守られている + 自動生成で実質File-based ルーティングな感じです。
微妙に感じる方もいるかもしれませんが、このアーキテクチャのおかげで、さまざまなメリットが生まれます。この後確認します!!!

大事なところ②:コンポーネントの登録

  component: () => <div>Hello /!</div>,

component: に対して、JSXを返却する関数を割り当てています。

以下のような書き方が可能です

  • 即時関数としてその場でコンポーネントを定義するパターン(自動生成されたやつはこれ)
src\routes\index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: () => <div>Hello /!</div>,
})
  • ファイル内でコンポーネントを定義するパターン
src\routes\index.tsx
import { createFileRoute } from '@tanstack/react-router'

// 先に変数として定義する必要がある(それはそう)
const jsx = () => {
  return <div>Hello /!</div>
}

export const Route = createFileRoute('/')({
  component: jsx
})
  • ファイル外のコンポーネントを引き渡すパターン
src\components\Message.tsx
export const Message = () => {
    return <div>Hello /!</div>
}
src\routes\index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { Message } from '../components/Message'

export const Route = createFileRoute('/')({
  component: () => <Message />
})

ルール ② route.tsxはディレクトリパス、ディレクトリ配下全てのパスにルーティングされる

route.tsxindex.tsx と同様にディレクトリパスにルーティングされます。
また、ディレクトリ配下全てのパスにもルーティングされます

mkdir src/routes/sample/
touch src/routes/sample/route.tsx

ディレクトリ、ファイルを作成します。

♻️  Regenerating routes...
🟡 Updating C:\work\react\my-tanStack-app\src\routes\sample\route.tsx
🟡 Updating C:\work\react\my-tanStack-app\src\routeTree.gen.ts
 Processed routes in 162ms

pluginが実行されます。Updateされたファイルを確認します

src\routes\sample\route.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/sample')({
  component: () => <div>Hello /sample!</div>,
})
src\routes\sample\route.tsx
export const Route = createFileRoute('/sample')({

/sample にルーティングされていることが確認できます。

cap3.PNG

いいかんじですね

ディレクトリ配下全てにルーティングされることも確認します。

mkdir src/routes/sample/child
touch src/routes/sample/child/index.tsx

http://localhost:5173/sample/child にアクセスしてみます。

cap7.PNG

Hello /sample/test! ではなく、Hello /sample! が表示されています。

ディレクトリ配下全てにルーティングされることが確認できました。
src/routes/sample/child/index.tsx の内容を表示するには、sample 側で <Outlet /> を使用する必要があります。

src\routes\sample\route.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/sample')({
    component: () => <><div>Hello /sample</div><Outlet /></>,
})

8.PNG

このように、 route.tsx はディレクトリ配下全てのルートで使用されることが確認できました。

ルール ③ index/route以外のXXX.tsxは、ディレクトリ/XXXパスにルーティングされる

ちょっとわかりづらいですが、以下のようにルーティングされます。

routes/test.tsx → /test にルーティング
routes/sample/test.tsx → /sample/test にルーティング

ややこしいですが、route.tsxindex.tsx が同時に存在するとバグるのと同じ理由で、
routes/test.tsx が存在する場合、 routes/test/index.tsx が存在すると、ルーティングが被ってバグります

以上がTanstack Routerの基本的なルーティングルールです。

では、index.tsxとroute.tsxとXXX.tsxはどう使い分ける?個人的な結論

それぞれの違いは下記記事がとても分かりやすいです。

結論:

  • route.tsxXXX.tsxはレイアウトコンポーネントとして使える
  • index.tsx はディレクトリのルートコンポーネントとしてのみ作用する

これらの特徴を鑑みて、以下の結論にしました!
① 基本的に index.tsx でパスに対するコンポーネントを作成する
route.tsx は特定パス配下で部分的に共通のレイアウトを使用したい場合に作成する(また、細かいパス管理は避ける)
③ どちらの役割なのかわからず、複雑になるため、XXX.tsx は基本的に使用しない

routes/
├── index.tsx           // → /
-├── about.tsx           // → /about
+└── about/
+      └── index.tsx    // → /about
└── blog/
+   ├── route.tsx       // <Outlet /> を使用し、/blog と /blog/:id に共通レイアウトを提供
    ├── index.tsx       // → /blog
    └── $id/
         └── index.tsx  // → /blog/:id (動的ルーティング)

図にするとこんな感じです!

tanstack routerの凄さ・ありがたさに少し触れる

File-based ルーティングのためだけなら、tanstack router以外にも選択肢があるので、
tanstack router の素晴らしい機能も体験してみます。

以下で紹介するtanstack router の素晴らしさは、
File-based ルーティングのすばらしさと直接関係しないものもあるので、
整理する際は注意してもらえればと思います!

素晴らしく型安全なルーティング

プレーンなreact-router-domだと、以下のようなコードがエラーとならず実行できてしまいます。

src/tekitou.tsx
import type { MouseEvent } from "react";

import { useNavigate } from 'react-router-dom';

export const SubmitButton = () => {

    const navigate = useNavigate();

    const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        navigate("/sonzaishinai_path", {
            state: {
                test: 1234,
                wakaran: 5678,
                kore_watashite_iino: "dame",
            }
        });
    };

    return (
        <button onClick={handleClick}>submit!</button>
    );
};

主な課題は2つあります。

  1. navigate先は全てのstringが許可されている
src/tekitou.tsx
        navigate("/sonzaishinai_path", {

許されちゃってますね

2.stateの型は特に設定されていない

src/tekitou.tsx
        navigate("/sonzaishinai_path", {
            state: {
                test: 1234,
                wakaran: 5678,
                kore_watashite_iino: "dame",
            }
        });

許されちゃってますね

では、Tanstack Router ではどうでしょう?確認してみます

テスト用コンポーネント準備

src\components\Message.tsx
interface MessageProps {
    message: string;
    year: number;
}

export const Message: React.FC<MessageProps> = ({ message, year }) => {
    return <div>{message} in {year}</div>
}

viewっぽい雰囲気のコンポーネントです。
messageyear を受け取って、div内で表示します。
routes配下のコンポーネントが、このコンポーネントを表示します。

src\routes\sample\route.tsx
import { createFileRoute } from '@tanstack/react-router'
import { Message } from '../../components/Message'

interface MessageProps {
  message: string;
  year: number;
}

const Sample = () => {
  const params = Route.useSearch()
  return <Message message={params.message} year={params.year} />
}

export const Route = createFileRoute('/sample')({
  validateSearch: (search: Record<string, unknown>): MessageProps => {
    return {
      message: String(search?.message ?? "not found"),
      year: Number(search?.year ?? 9999)
    }
  },
  component: Sample
})

src\routes\sample\route.tsx を、Messafe.tsx をレンダリングするよう修正しました。
重要なところが2点あるので、解説します。

① validateSearch でstateをURLクエリパラメータとして表現

src\routes\sample\route.tsx
  validateSearch: (search: Record<string, unknown>): MessageProps => {
    return {
      message: String(search?.message ?? "not found"),
      year: Number(search?.year ?? 9999)
    }
  },

Tanstack Router では、localなstateはURLクエリパラメータを介してやり取りすることが推奨されています。
(また、DBの情報などの、サーバーサイドのstateは別のTanstack Queryというライブラリで管理することが推奨されています。こっちも記事書きたい)

クエリパラメータとしてstateを引き渡すために、この validateSearch を利用することができます。

validateSearch の名前の通り、高度なvalidateも実装できるのですが、いったん今回はスコープ外として、さっくり解説します。

src\routes\sample\route.tsx
  validateSearch: (search: Record<string, unknown>): MessageProps => {

serchパラメーターを受け取って、その中からどういったオブジェクトを取得するか?を記載しています。
serchパラメーターはユーザーまたは遷移元が任意の値を付与できるので、search: Record<string, unknown> となっています。

src\routes\sample\route.tsx
    return {
      message: String(search?.message ?? "not found"),
      year: Number(search?.year ?? 9999)
    }
  },

受け取ったserchパラメーターを整形し、Messageコンポーネントが必要なpropsの型のオブジェクトを返却します。
今回はserchパラメーターにmessageとyearがない場合適当な値を代入し、エラーをハンドリングしています。

② useSearch関数でURLクエリパラメータを取得する

src\routes\sample\route.tsx
  const params = Route.useSearch()

Routeから、整形されたserchパラメーターを取得しています。
paramsの型は、Routeの validateSearch の戻り値なので、ここでは { message: string; year: number; } となります。

cap4.PNG

型安全ですね~。

長くなりましたが、呼び出される側の準備はこんな感じです。

では、このルートへ、useNavigate関数などで移動してみます。

useNavigate関数

tanstack routerにも useNavigate関数が用意されています。

実装してみるとこんな感じになります

navigate.gif

どうですか?
pathstate も、なにもかも型安全なことが分かったのではないでしょうか?!

src\components\Button.tsx
import type { MouseEvent } from "react";

import { useNavigate } from '@tanstack/react-router';

export const SubmitButton = () => {

    const navigate = useNavigate();

    const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        navigate({ to: "/sample", search: { message: "type safe!", year: 3150 } });
    };

    return (
        <button onClick={handleClick}>submit!</button>
    );
};

Linkコンポーネント

Tanstack Routerはリンクコンポーネントも提供してくれています。

navigate関数と同様、型安全が保障されています。

cap5.PNG

cap6.PNG

src\components\MoveLink.tsx
import { Link } from "@tanstack/react-router"

export const MoveLink = () => {
    return <Link to={"/sample"} search={{ message: "reiwa", year: 2019 }} />
}

どうだったでしょうか?
File-based ルーティングもできるし、型安全な開発も加速するし、最高!!となったでしょうか。

TanStack Routerには他にもloaderやlazyなど色んな機能がありますが、いったんここでは触れまないでおこうと思います!。

終わりに

今回はTanStack Routerを軽く使ってみました。
テストコードを書くとライブラリがより深く理解できるので、後日テスト編も記事にしようと思います。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?