File-based ルーティングの開発体験を紹介
File-based ルーティングの世界では、こんな感じの開発が行えます。
少しでも興味をもってもらえたでしょうか?
この記事では以下の構成でFile-based ルーティングに入門します
① File-based ルーティングって?
② 実際にReactでFile-based ルーティングしてみる!~with TanStack Router~
③ TanStack Router をvitestでテストして、仕組みを理解する(別記事)
やっていきます!
① File-based ルーティングって?
File-based ルーティングについて、少し深掘りしてみます!!
File-based ルーティングとは、jsxファイルのルーティングを、コードでの設定ではなく、
物理的なディレクトリの位置で行おう!という考え方です。
コードベースのルーティングとファイルベースのルーティングを比較して、どんなものかイメージしてみます。
コードベースのルーティング(React-router-dom)
<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を再掲します
routes/test/sample.tsx
→ /test/sample
でアクセス可能になっていることが確認できます。
良い感じですね
やっていきましょう!
今回はこんな感じでTanStack Routerを触っていきます
- vite + React開発環境の作成
- tanstack routerをインストールする
- tanstack routerをセットアップする
- tanstack routerを使ってFile-based ルーティング!!!
- 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/
にアクセスします
良い感じですね
では早速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をインポートして、追加する感じですね
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
の初期化をおこなってくれてます。中身を見てみます
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 />
とするとよさそうですね
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アプリ内部で使用するようコードを修正します
ここもとても大事なのですが、細かい解説はテストコードを書く時に記載するので割愛します!
とりあえずコピペで大丈夫です
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
なんか成功っぽいログが出てます。いいですね
- [✓] vite + React開発環境の作成
- [✓] tanstack routerをインストールする
- [✓] tanstack routerをセットアップする
- [] tanstack routerを使ってFile-based ルーティング!!!
- [] tanstack routerの凄さ・ありがたさに少し触れる
ここまで長かった・・・
tanstack routerを使ってFile-based ルーティング!!!
起動している http://localhost:5173/
を確認します。
__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
どんな感じか見に行きましょう
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Hello /!</div>,
})
色々記載されてますね。大事なところだけ確認します
大事なところ①:ルートの作成
export const Route = createFileRoute('/')({
自動生成でルートへの登録が行われている部分です
いや、結局コードベースで設定しとんかーい、というツッコミを感じる方もいるかもしれませんが、
この記述はpluginが行ってくれます。また、引数を勝手に変えると怒られます。
型で守られている + 自動生成で実質File-based ルーティングな感じです。
微妙に感じる方もいるかもしれませんが、このアーキテクチャのおかげで、さまざまなメリットが生まれます。この後確認します!!!
大事なところ②:コンポーネントの登録
component: () => <div>Hello /!</div>,
component:
に対して、JSXを返却する関数を割り当てています。
以下のような書き方が可能です
- 即時関数としてその場でコンポーネントを定義するパターン(自動生成されたやつはこれ)
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Hello /!</div>,
})
- ファイル内でコンポーネントを定義するパターン
import { createFileRoute } from '@tanstack/react-router'
// 先に変数として定義する必要がある(それはそう)
const jsx = () => {
return <div>Hello /!</div>
}
export const Route = createFileRoute('/')({
component: jsx
})
- ファイル外のコンポーネントを引き渡すパターン
export const Message = () => {
return <div>Hello /!</div>
}
import { createFileRoute } from '@tanstack/react-router'
import { Message } from '../components/Message'
export const Route = createFileRoute('/')({
component: () => <Message />
})
ルール ② route.tsxはディレクトリパス、ディレクトリ配下全てのパスにルーティングされる
route.tsx
も index.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されたファイルを確認します
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/sample')({
component: () => <div>Hello /sample!</div>,
})
export const Route = createFileRoute('/sample')({
/sample
にルーティングされていることが確認できます。
いいかんじですね
ディレクトリ配下全てにルーティングされることも確認します。
mkdir src/routes/sample/child
touch src/routes/sample/child/index.tsx
http://localhost:5173/sample/child
にアクセスしてみます。
Hello /sample/test!
ではなく、Hello /sample!
が表示されています。
ディレクトリ配下全てにルーティングされることが確認できました。
src/routes/sample/child/index.tsx
の内容を表示するには、sample 側で <Outlet />
を使用する必要があります。
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/sample')({
component: () => <><div>Hello /sample</div><Outlet /></>,
})
このように、 route.tsx
はディレクトリ配下全てのルートで使用されることが確認できました。
ルール ③ index/route以外のXXX.tsxは、ディレクトリ/XXXパスにルーティングされる
ちょっとわかりづらいですが、以下のようにルーティングされます。
routes/test.tsx → /test にルーティング
routes/sample/test.tsx → /sample/test にルーティング
ややこしいですが、route.tsx
と index.tsx
が同時に存在するとバグるのと同じ理由で、
routes/test.tsx
が存在する場合、 routes/test/index.tsx
が存在すると、ルーティングが被ってバグります
以上がTanstack Routerの基本的なルーティングルールです。
では、index.tsxとroute.tsxとXXX.tsxはどう使い分ける?個人的な結論
それぞれの違いは下記記事がとても分かりやすいです。
結論:
route.tsx
とXXX.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だと、以下のようなコードがエラーとならず実行できてしまいます。
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つあります。
- navigate先は全てのstringが許可されている
navigate("/sonzaishinai_path", {
許されちゃってますね
2.stateの型は特に設定されていない
navigate("/sonzaishinai_path", {
state: {
test: 1234,
wakaran: 5678,
kore_watashite_iino: "dame",
}
});
許されちゃってますね
では、Tanstack Router ではどうでしょう?確認してみます
テスト用コンポーネント準備
interface MessageProps {
message: string;
year: number;
}
export const Message: React.FC<MessageProps> = ({ message, year }) => {
return <div>{message} in {year}</div>
}
viewっぽい雰囲気のコンポーネントです。
message
と year
を受け取って、div内で表示します。
routes配下のコンポーネントが、このコンポーネントを表示します。
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クエリパラメータとして表現
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も実装できるのですが、いったん今回はスコープ外として、さっくり解説します。
validateSearch: (search: Record<string, unknown>): MessageProps => {
serchパラメーターを受け取って、その中からどういったオブジェクトを取得するか?を記載しています。
serchパラメーターはユーザーまたは遷移元が任意の値を付与できるので、search: Record<string, unknown>
となっています。
return {
message: String(search?.message ?? "not found"),
year: Number(search?.year ?? 9999)
}
},
受け取ったserchパラメーターを整形し、Messageコンポーネントが必要なpropsの型のオブジェクトを返却します。
今回はserchパラメーターにmessageとyearがない場合適当な値を代入し、エラーをハンドリングしています。
② useSearch関数でURLクエリパラメータを取得する
const params = Route.useSearch()
Routeから、整形されたserchパラメーターを取得しています。
paramsの型は、Routeの validateSearch
の戻り値なので、ここでは { message: string; year: number; }
となります。
型安全ですね~。
長くなりましたが、呼び出される側の準備はこんな感じです。
では、このルートへ、useNavigate関数などで移動してみます。
useNavigate関数
tanstack routerにも useNavigate関数が用意されています。
実装してみるとこんな感じになります
どうですか?
path
も state
も、なにもかも型安全なことが分かったのではないでしょうか?!
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関数と同様、型安全が保障されています。
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を軽く使ってみました。
テストコードを書くとライブラリがより深く理解できるので、後日テスト編も記事にしようと思います。