既存の Vite + React プロジェクトに TanStack Router をあとから導入する
はじめに
Vite + React で作った単一ページのサイトを拡張する必要が出てきたので、そのためルーティングを実装したい。
このルーティングをやってくれるライブラリとして TanStack Router というものがあり、以前から気になっていたので使ってみることにした。
だが、まっさらな状態から導入する例や機能を端的にまとめた記事などが多く、どうやって途中から導入していけばいいのかよくわからなかったので備忘録がてら記事を書いてみた。とはいえ、基本的にはpnpm create vite
の状態からの構築方法としても読めるようにしたので、参考になれば幸いだ。
前提
-
pnpm create vite
などで Vite の React プロジェクトが作成されている。(react-swc-ts
) - パッケージマネージャーとして
pnpm
を筆者は使っているが、npm
などの他のツールに読み替えても特に問題はないと思われる。 - Vite や React についての基礎的な知識はあるものとする。
- TanStack Router v1 を用いる。
- File-based なルーティングを採用する。
準備
まずは以下のコマンドから TanStack Router をインストールする。
pnpm add @tanstack/react-router
pnpm add -D @tanstack/router-plugin
公式では@tanstack/router-devtools
も入れる誘導になっているが、個人的にはすでに公開しているサイトだったのでこの記事では使わない。
次に Vite の設定ファイルを以下のように修正する。
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
+ import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), TanStackRouterVite()],
});
TanStack Router の導入
main.tsx
の変更
まずは破壊的な変更を加えていく。TanStack Router を使う上でsrc/main.tsx
を以下の内容をコピペして全て書き換える。
import { 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>,
)
}
これは公式の導入にもあり、そこからそのままコピペしている。
./routeTree.gen
がない!と Linter から怒られるだろうが、routeTree.gen.ts
というファイルは開発サーバーを立ち上げる時に自動生成されるので無視しよう。
Root の設定
次にsrc/routes
フォルダを作成する。このフォルダがルーティングの起点になる。
そして、その中に以下のような__root.tsx
ファイルを作る。
import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
</>
),
});
<Outlet />
は現在の位置に対して子にあるコンポーネントを表すものである。まあ、children
のようなものである。ここで定義されているのは全てのルートの根(Root)なので、ページ全てに適用されるものを記述する。
例えば MUI などを使っていてページ全体に常に適用したい(表示したい)要素などがあれば、<Outlet />
コンポーネントを囲むように以下のように配置する。
import { createRootRoute, Outlet } from "@tanstack/react-router";
+ import { ThemeProvider, CssBaseline, createTheme } from "@mui/material";
+ const theme = createTheme({ /* ... */ });
export const Route = createRootRoute({
component: () => (
<>
+ <ThemeProvider theme={theme}>
+ <CssBaseline />
<Outlet />
+ </ThemeProvider>
</>
),
});
App.tsx
の移動
おそらく SPA でそのままページを作っているのであればApp.tsx
を使っているのではないかと思われるので、これを認識させる方法をとる。App.tsx
でなくてもページを構成するメインのコンポーネントがあるファイルを移動する。
File-based なルーティングを採用するので、ルーティングの起点(src/routes
)のファイル、つまりindex.tsx
として配置しなおす。
mv src/App.tsx src/routes/index.tsx
こうすることで、トップページに表示されるコンポーネントとして認識される。
もし、複数のファイルにコンポーネントを分割している場合、構成要素はsrc/routes/-components
フォルダに移動するのが良いだろう。
mv src/components/Example.tsx src/routes/-components/Example.tsx
-components
フォルダの-
はルートとして認識させないための prefix である。[1]これがないと例えばsrc/routes/components/Example.tsx
はhttps://<frontend>/components/Example
という URL にアクセスできることになってしまう。
これが終わったら各ファイル内のimport
のパスを解決して、index.tsx
に以下の内容を追加する。
/* imports */
+ import { createFileRoute } from "@tanstack/react-router";
// トップページに表示されるコンポーネント
function App() {
/* コンポーネントの実装 */
}
- export default App;
+ export const Route = createFileRoute("/")({
+ component: App,
+ });
Linter がcreateFileRoute
の引数"/"
に対してエラーを指摘してくるが、これもrouteTree.gen.ts
が生成されることで解決する。逆にrouteTree.gen.ts
さえ生成されてしまえば、こういったルーティングで利用する文字列は今後補完が効くようになる。(これが便利!)
開発サーバーを立ち上げる
これで準備が整ったので以下のコマンドから開発サーバーを立ち上げよう。
pnpm dev
するとsrc/routeTree.gen.ts
が自動的に生成され、localhost
を開けば期待通りトップページに設定したコンポーネントが描画されているはずである。
もちろん、今まで Linter に指摘されていたところも指摘してこなくなるだろう。ちなみに開発サーバーを立ち上げている間はsrc/routes
フォルダは監視され、ルーティングを追従して解決してくれる。
これで、既存の React アプリを TanStack Router に乗せることはできた。
ルーティング機能を使ってみる
これで当初の目的である既存の React アプリに TanStack Router を導入した。これでおしまい、といってもいいのだが、さすがにルーティング機能が使われていないままなのはよろしくない。
ただ、機能紹介は他の方が詳細な記事[2]を書いていたり、そもそも公式を参照すれば良いので、個人的に良かった(使った)機能を紹介する。
NotFound ページをカスタマイズ
現状のままではルートがトップページしかないので、/path/to/page
のようなパスのページを見に行こうとしても"Not Found"とそっけないメッセージがあるだけである。
例えば、src/NotFound.tsx
ファイルを作成し、以下のようなコンポーネントを定義する。
export function NotFound() {
return <>ここには何もないですよ。</>;
}
(これも大して変わらんだろ。)
NotFound ページの書き換えは以下のようにmain.tsx
を書き換えてできる。
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
+ import { NotFound } from "./NotFound";
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
- const router = createRouter({ routeTree })
+ const router = createRouter({
+ routeTree,
+ defaultNotFoundComponent: NotFound,
+ });
/* これ以降は同じ */
ちなみにエラーページやローディングページなども同様の方法で実装できる。ただ、ここで設定されるコンポーネントは本当に全てに適応されるものであって、それとは別に特定のルートにおける実装もできる。
詳細は以下の公式の説明を見ると良い。
loader によるデータフェッチと結果による切り替え
静的サイトではなく、何らかのバックエンドと通信する動的サイトを作ることを考える。
例えば、https://<frontend>/articles/<articleId>
を開くとarticleId
に一致する文章を表示するようなものを考える。
src/routes/articles/$articleId.tsx
ファイルを作成すると、(開発サーバーを立ち上げたままなら)以下のようなテンプレートが自動で生成される。
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/articles/$articleId')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/articles/$articleId"!</div>
}
$articleId
の$
は動的パスパラメータを表し、ここに指定された文字列を値として受け取れるようになる。[1:1]
例えば以下のように書き換えると、指定されたarticleId
があればその内容を表示し、なければnotFound
エラーで NotFound ページを表示できる。
- import { createFileRoute } from '@tanstack/react-router'
+ import { createFileRoute, notFound } from "@tanstack/react-router";
+ type Article = {
+ title: string;
+ author: string;
+ text: string;
+ };
export const Route = createFileRoute("/articles/$articleId")({
+ loader: async ({ params: { articleId } }) => {
+ try {
+ const response = await fetch(`https://<backend>/articles/${articleId}`);
+ return (await response.json()) as Article; // 例のためであって、あまりやってはいけない記法
+ } catch {
+ throw notFound();
+ }
+ },
component: RouteComponent,
+ notFoundComponent: () => {
+ const { articleId } = Route.useParams();
+ return <>'{articleId}' で指定された文章はありませんよ</>;
+ },
});
function RouteComponent() {
+ const article: Article = Route.useLoaderData();
- return <div>Hello "/articles/$articleId"!</div>
+ return (
+ <div>
+ <h1>{article.title}</h1>
+ <h3>{article.author}</h3>
+ {article.text}
+ </div>
+ );
+ }
リダイレクト
リダイレクトも簡単で、トップページから上の例で作った特定のarticleId
のページに飛ばす時は以下のようにsrc/routes/index.tsx
を修正すれば良い。
- import { createFileRoute } from "@tanstack/react-router";
+ import { createFileRoute, redirect } from "@tanstack/react-router";
- // トップページに表示されるコンポーネント
- function App() {
- /* コンポーネントの実装 */
- }
export const Route = createFileRoute("/")({
+ loader: () => {
+ throw redirect({
+ to: "/articles/$articleId",
+ params: {
+ articleId: "introduction",
+ },
+ });
+ },
- component: App,
});
終わりに
パスやパスパラメータにバッチバチに推論が効くので TanStack Router の使い心地はとても良かった。今後も使っていきたい。
それと、本記事の具体的なモチベーションについては別記事として書いたので、もし興味があれば以下の記事も見ていただけるとありがたい。
Discussion