TanStack Routerでサクッと始める型安全ルーティング
はじめに
こんにちは。calloc134 です。
自分は普段 React を利用してフロントエンドの開発をしています。
SPA のルーティングを実装する上で、TanStack Router を利用することが多いです。
この使い方について、簡単にまとまったドキュメントが思ったより少なく感じたため、まとめていきたいと思います。
TanStack Router とは
TanStack Router は、React のルーティングを行うためのライブラリです。
当初は React Location として、TanStack の Tanner Linsley 氏によって開発されました。
その後、改名や設計のし直しが行われ、TanStack Router として開発されており、2023 年のクリスマスに v1 がリリースされました。
現在は色々な機能が追加されており、React のルーティングを行うためのライブラリとして非常に人気があります。
TanStack Router の特徴
型安全の偉大さ
TanStack Router は、なんといっても型安全性が非常に高いです。
TanStack Router では、リンクコンポーネントやリダイレクト時にパスを指定することができます。このとき、存在しているルーティングのパスだけを型として受け付け、補完やエラーを出すことができます。
また、動的ルートやクエリパラメータの型についても安全に扱うことができます。
始めてみる
では、実際に利用してみましょう
なお、公式ではファイルベースのルーティングを推奨していますが、今回は説明を簡単にするため、コードベースでのルーティングを行います。
また、パッケージマネージャーには pnpm を利用しています。
セットアップ
pnpm create vite tanstack-router-lab --template react-ts
cd tanstack-router-lab
pnpm install
App.tsx を適当なコンポーネントに書き換えます。
export const Hello = () => {
return (
<div>
<h1>Hello World</h1>
<p>Click on the links above to see the code splitting in action.</p>
</div>
);
};
次に、TanStack Router をインストールします。
pnpm add @tanstack/react-router
ルーティングの記述
では、main.tsx
と同じディレクトリにroute.tsx
を作成し、以下のように記述します。
import { createRootRoute, createRouter } from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute({
component: () => <Hello />,
});
const routeTree = rootRoute;
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ここで、コードの解説をしていきます。
createRootRoute
は、根本のルートを作成する関数です。
routeTree
とは、ルーティングのツリー構造を表す変数です。今回は、rootRoute
だけを指定しています。
createRouter
にrouteTree
を渡すことで、ルーティングを行うためのrouter
が作成されます。
ここで、createRootRoute
の引数に component
を指定していますが、これはルートにコンポーネントを指定するためのものです。
では、main.tsx
にRouterProvider
を追加し、ルーティングができるようにします。
import { RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { router } from "./route";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);
ここでは、先程作成したrouter
をRouterProvider
に渡すことで、ルーティングが有効になります。
vite サーバを立ち上げると、Hello
コンポーネントの内容が表示されるはずです。
ここの部分の内容は以下のリポジトリにあります。
pnpm run dev
開発者ツールの追加
TanStack Router には、開発者ツールが用意されています。
このコンポーネントを追加することで、ルーティングの情報を確認することができます。
pnpm add @tanstack/router-devtools
導入後、main.tsx
を以下のように修正します。
import { RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { router } from "./route";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
<TanStackRouterDevtools router={router} />
</StrictMode>
);
これで、開発者ツールが表示されるようになります。
便利なので是非導入してみてください。
なお、RouterProvider
の内部でTanStackRouterDevtools
を利用する場合はrouter
を渡す必要がなく、自動的に取得されます。
今回はまだRouterProvider
内部でネストされたルーティングを設定していないため、RouterProvider
の外でrouter
を渡しています。
一般的なルート
一般的にはRoot
にコンポーネントを指定せず、Root
以下に別途ルーティングを記述します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute({});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: () => <Hello />,
});
const routeTree = rootRoute.addChildren([indexRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
createRoute
は、根本でないルートを作成する関数です。
createRoute
では、getParentRoute
とpath
が必須となっています。
getParentRoute
は、親のルートを指定する関数です。
path
は、ルーティングのパスを指定します。
component
には先程と同様にコンポーネントを指定します。
また、routeTree
には、rootRoute
に子のルートを追加したものを指定します。addChildren
メソッドを利用することで、子のルートを追加することができます。
このように記述することで、/
にアクセスしたときに Hello
が表示されるようになります。
リンクやリダイレクトの型安全
TanStack Router では、リンクやリダイレクトを行うためのコンポーネントが提供されています。
試しに、App.tsx
にリンクを追加してみましょう。
Link
コンポーネントを利用することで、リンクを作成することができます。
import { Link } from "@tanstack/react-router";
export const Hello = () => {
return (
<div>
<h1>Hello World</h1>
<p>Click on the links above to see the code splitting in action.</p>
<Link to="/">Go to Home</Link>
</div>
);
};
すると、ルーティングに存在するパスを型として受け付け、補完が効くようになります。
また、リンクが追加されます。
/
にアクセスし、リンクをクリックすると、ページが遷移することが確認できます。
また、TanStack Router ではuseNavigate
というフックも提供されています。
このフックの返すnavigate
関数を利用することで、リダイレクトを行うことができます。
import { Link, useNavigate } from "@tanstack/react-router";
export const Hello = () => {
const navigate = useNavigate();
return (
<div>
<h1>Hello World</h1>
にコンポーネントを指定せず、ルート以下にルーティングを別途記述します。
<p>Click on the links above to see the code splitting in action.</p>
<Link to="/">Go to Home</Link>
<button
onClick={() =>
navigate({
to: "/",
})
}
>
Go to Lazy
</button>
</div>
);
};
navigate
関数はオブジェクトを引数に取り、to
プロパティにリダイレクト先のパスを指定します。
ここでも、型安全にリダイレクト先のパスを指定することができます。
/
にアクセスしてボタンを押すと、リダイレクトが行われることが確認できます。
クエリ文字列については複雑であるため、後から解説します。
可能なルーティングのパターン
ネストされたルート
TanStack Router では、ネストしたルートを簡単に作成することができます。
また、ネストしたルートのコンポーネント内で<Outlet>
コンポーネントを利用することで、外枠となるレイアウトを簡単に実装することができます。
<Outlet>
コンポーネントの使い方は、同じルーティングライブラリのreact-router
と似ています。
では、Layout.tsx
を作成し、以下のように記述します。
export const Layout = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<header>
<h1>I'm a header</h1>
</header>
{children}
</div>
);
};
children で受け取ったコンポーネントを表示するだけのシンプルなコンポーネントです。ヘッダが付いています。
では、route.tsx
を記述してルーティングを修正します。
import {
createRootRoute,
createRoute,
createRouter,
Outlet,
} from "@tanstack/react-router";
import { Hello } from "./App";
import { Layout } from "./Layout";
const rootRoute = createRootRoute({
component: () => (
<Layout>
<Outlet />
</Layout>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: () => <Hello />,
});
const routeTree = rootRoute.addChildren([indexRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
Layout
コンポーネントをrootRoute
に指定し、その中に<Outlet>
コンポーネントを配置しました。
これで、Layout
コンポーネントが表示されるようになります。
/
にアクセスしましょう。
Hello
コンポーネントがLayout
コンポーネントの中に表示されていることがわかります。
ネストされたルートの基本的な使い方としては、親のルートで外枠となるコンポーネントを指定し、children に<Outlet>
コンポーネントを指定するのが好ましいでしょう。
その上で、子のルートにコンポーネントを指定することで、外枠のコンポーネントに子のコンポーネントが埋め込まれる形となります。
これを応用して、特定のパス以下にネストされたルートを作成することも可能です。
route.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
Outlet,
} from "@tanstack/react-router";
import { Hello } from "./App";
import { Layout } from "./Layout";
const rootRoute = createRootRoute({});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hello",
component: () => (
<Layout>
<Outlet />
</Layout>
),
});
const helloRoute = createRoute({
getParentRoute: () => indexRoute,
path: "/foo",
component: () => <Hello />,
});
const routeTree = rootRoute.addChildren([indexRoute.addChildren([helloRoute])]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
これで、/hello/foo
にアクセスしたときにHello
コンポーネントが表示されるようになります。
また、/hello
にアクセスしたときに、Layout
コンポーネントのヘッダが表示されるのがわかります。なお、中身については空であるため、何も表示されません。
パス無しのルート
TanStack Router では、パス無しのルートを作成することができます。
これは、ルーティングの観点からはあまり意味のない機能です。
しかし、ルーティングの構造を関心事でまとめたい場合には有用であると考えられます。
使い方としては先程のネストされたルートに似ていますが、パスを指定せず、id を指定するところが異なります。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
const rootRoute = createRootRoute({});
const greetingRoute = createRoute({
getParentRoute: () => rootRoute,
id: "greeting",
});
const helloRoute = createRoute({
getParentRoute: () => greetingRoute,
path: "/hello",
component: () => <p>Hello</p>,
});
const byeRoute = createRoute({
getParentRoute: () => greetingRoute,
path: "/bye",
component: () => <p>Bye</p>,
});
const routeTree = rootRoute.addChildren([
greetingRoute.addChildren([helloRoute, byeRoute]),
]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ルーティングとしては、/hello
と/bye
が存在しますが、これらをまとめて扱いたいものです。
この場合には、パス無しルーティングを利用することで、greeting
というグループを作成し、その子ルートとして/hello
と/bye
を追加することができます。
/hello
にアクセスすると、Hello
が表示されます。
/bye
にアクセスすると、Bye
が表示されます。
動的ルートとパラメータ取得
TanStack Router では、動的ルートを作成することができます。
動的ルートとは、パスの一部を動的に変更することができるルートのことです。
パスに$
から始まる変数を指定することで、動的ルートを作成することができます。
イメージとしてはワイルドカードのようなものです。
では、route.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute({});
const helloRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/$helloId",
component: () => <Hello />,
});
const routeTree = rootRoute.addChildren([helloRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
helloId
という変数を$
で指定しています。
/(任意の文字列)
にアクセスし、Hello
が表示されることを確認します。
更に、コンポーネント内部でuseParams
フックを利用することで、パラメータを取得することができます。
import { useParams } from "@tanstack/react-router";
export const Hello = () => {
const { helloId } = useParams({ from: "/$helloId" });
return (
<div>
<h1>Hello World</h1>
<p>Click on the links above to see the code splitting in action.</p>
<p>
The current helloId is: <strong>{helloId}</strong>
</p>
</div>
);
};
useParams
フックは、オブジェクトを引数に取り、from
プロパティに現在のパスを指定します。
では、/(任意の文字列)
にアクセスし、パラメータが表示されることを確認します。
正常にパラメータが取得できていることが確認できます。
コンポーネントの指定
TanStack Router では、いくつかの特徴的なコンポーネントが指定できます。
今までの例では、component
プロパティにコンポーネントを指定していました。
これ以外にも、ルートにおいて指定できるコンポーネントが存在します。
Not Found コンポーネント
存在しないルートであったときに表示されるコンポーネントです。
親のルートか、一番上のルートに指定します。
今回は、一番上のrootRoute
に指定します。
route.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute({
notFoundComponent: () => <div>Not Found... sorry</div>,
});
const helloRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hello",
component: () => <Hello />,
});
const routeTree = rootRoute.addChildren([helloRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
では、/hello
以外のパスにアクセスしてみましょう。
このように、存在しないパスにアクセスしたときにNot Found... sorry
が表示されることが確認できます。
エラーコンポーネント
通常コンポーネントがエラーを起こしたときに表示されるコンポーネントです。
App.tsx
にエラーを起こす処理を追加します。
export const Hello = () => {
// ランダムでエラーを発生させる
if (Math.random() > 0.5) {
throw new Error("Random error");
}
return (
<div>
<h1>Hello World</h1>
<p>Click on the links above to see the code splitting in action.</p>
</div>
);
};
route.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute({
notFoundComponent: () => <div>Not Found... sorry</div>,
});
const helloRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hello",
component: () => <Hello />,
errorComponent: () => <div>There was an error!! Sorry</div>,
});
const routeTree = rootRoute.addChildren([helloRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
では、/hello
にアクセスしてみましょう。
50%の確率で、正常なコンポーネントが表示されることが確認できます。
エラーが発生したときに、There was an error!! Sorry
が表示されることが確認できます。
指定したエラーコンポーネントが表示されることが確認できます。
Pending コンポーネント
Pending コンポーネントは、通常コンポーネントが読み込まれるまで表示されるコンポーネントです。
これを解説する前に、React のサスペンドと Suspense コンポーネントについて簡単に説明します。
React コンポーネント内部で初期データの取得などを行う場合、データが取得されるまでコンポーネントを「ローディング中なのでレンダリングできない」という状態にすることができます。
この状態をサスペンドといいますが、この状態ではコンポーネントを表示できません。
そのため、このような状態になりうるコンポーネントをラップし、中身のコンポーネントがサスペンド状態である場合に別のコンポーネントを表示させることができるラッパーとなってくれるコンポーネントがSuspense
です。
<Suspense fallback={<div>サスペンドしたらこれが表示される</div>}>
{/* ↓サスペンドしなかったらこれが表示される */}
<MyComponent />
</Suspense>
TanStack Router では、Pending コンポーネントを指定することで、自前で Suspense コンポーネントをラップすることなく、サスペンドしたら表示されるコンポーネントを指定することができます。
したがって、サスペンドなコンポーネントをより簡単に利用することができるのです。
試しに、データ取得を非同期で行う実装で試してみましょう。
データ取得を支援する実装として、TanStack Query を利用します。
データの取得は fetch 関数で行うのですが、その間にコンポーネントをサスペンドさせることができます。
pnpm add @tanstack/react-query
まず TanStack Query を初期化するため、main.tsx
に以下のように記述します。
import { RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { router } from "./route";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<TanStackRouterDevtools router={router} />
</QueryClientProvider>
</StrictMode>
);
次に、App.tsx
を以下のように修正します。
import { useSuspenseQuery } from "@tanstack/react-query";
export const Hello = () => {
// 3秒遅延して返却されるエンドポイントを叩いてみる
const { data } = useSuspenseQuery({
queryKey: ["hello"],
queryFn: async () => {
const response = await fetch("https://httpbin.org/delay/3");
return response.json();
},
});
return (
<div>
<h1>Hello World</h1>
<p>Click on the links above to see the code splitting in action.</p>
<p>{JSON.stringify(data)}</p>
</div>
);
};
useSuspenseQuery
フックを利用することで、非同期処理を行い、データの取得中にコンポーネントをサスペンドすることができます。
https://httpbin.org/delay/3
とは、3 秒遅延してデータを返却するエンドポイントです。
最後に、route.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute({
notFoundComponent: () => <div>Not Found... sorry</div>,
});
const helloRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hello",
component: () => <Hello />,
pendingComponent: () => <div>Loading...Please wait</div>,
errorComponent: () => <div>There was an error!! Sorry</div>,
});
const routeTree = rootRoute.addChildren([helloRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
pendingComponent
プロパティに、データ取得中に表示されるコンポーネントを指定します。
では、/hello
にアクセスしてみましょう。
Loading...Please wait
が表示されることが確認できます。
3 秒後にデータが取得され、表示されることが確認できます。
公式が提供しているその他の機能
クエリパラメータとバリデーション
TanStack Router では、クエリパラメータを取得することができます。
ドキュメントでは、search params
と呼ばれています。
では、main.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
const rootRoute = createRootRoute();
const helloRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hello",
component: () => <Hello />,
validateSearch: (search: Record<string, unknown>) => {
return {
hoge: search.hoge as string,
};
},
});
const routeTree = rootRoute.addChildren([helloRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
validateSearch
プロパティには、クエリパラメータをバリデーションする関数を指定します。
この関数は引数として連想配列を受け取ります。キーはstring
、値はunknown
です。
クエリはhoge=fuga
のようにkey=value
の形式で渡されるため、search.hoge
として取得することができます。
この値は undefined である可能性や、型として不適切な値が渡される可能性があるため、適切な型に変換する必要があります。
したがって、バリデーションを行う関数が必要になるのです。
この例では、as string
を行なって無理やり型を変換していますが、実際には適切なバリデーションを行う必要があります。
では、App.tsx
を以下のように修正します。
import { useSearch } from "@tanstack/react-router";
export const Hello = () => {
const { hoge } = useSearch({ from: "/hello" });
return (
<div>
<h1>Hello World</h1>
<p>Click on the links above to see the code splitting in action.</p>
<p> hoge: {hoge}</p>
</div>
);
};
useSearch
フックを利用することで、クエリパラメータを取得することができます。
ここで、フックの戻り値は、validateSearch
関数で指定した型となっていることがわかります。
では、実際にアクセスしてみましょう。
今回は/hello?hoge=fuga
にアクセスしてみます。
正常にクエリパラメータが取得できていることが確認できます。
応用として、バリデーションライブラリを利用してみましょう。
今回は、valibot を利用します。
pnpm add valibot
main.tsx
を以下のように修正します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { Hello } from "./App";
import { number, object, parse } from "valibot";
const rootRoute = createRootRoute();
const querySchema = object({
hoge: number(),
});
const helloRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/hello",
component: () => <Hello />,
validateSearch: (search: Record<string, unknown>) => {
return parse(querySchema, search);
},
});
const routeTree = rootRoute.addChildren([helloRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
こうすることで、valibot によるバリデーションが行われるようになります。
useSearch
フックの型も変更され、バリデーションの解析後の型が返されるようになります。
遅延読み込みで効率化
TanStack Router では、初期状態で読み込まなくて良いオプションを遅延読み込みすることができます。
これにより、初期読み込みを軽減することができ、パフォーマンスの向上につながります。
ここでは、一般的なルートに対する遅延読み込みを行ってみます。
まず、遅延読み込みしたいルートについて、別途ファイルとして切り出します。
ここでは、IndexLazyRoute.tsx
として切り出します。
import { createLazyRoute } from "@tanstack/react-router";
import { Hello } from "./App";
export const indexLazyRoute = createLazyRoute("/")({
component: () => <Hello />,
});
このようにして、遅延読み込みされるルート(以下、遅延ルート)を作成します。
次に、route.tsx
に以下のように記述します。
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
const rootRoute = createRootRoute({});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
}).lazy(() => import("./IndexLazyRoute").then((d) => d.IndexLazyRoute));
const routeTree = rootRoute.addChildren([indexRoute]);
export const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
これで、/
にアクセスしたときに Hello
が表示されるようになります。
ここでは初期ページであるためコンポーネントはすぐ読み込みされ、遅延の効果は感じられません。
しかし、初期ページ以外のページに適用することでパフォーマンスの向上が期待できます。
createLazyRoute
は、遅延読み込みするための関数です。
ルーティングパスを指定して遅延読み込みするためのオプションをここに記述することにより、オプションの遅延読み込みが可能です。
通常ルートからlazy
メソッドを呼び出し、遅延読み込みするファイルを指定することで、遅延読み込みを行うことができます。
では、どのようなオプションは遅延するべきなのでしょうか?
公式ドキュメントでは、遅延するべきものを以下のように定義しています。
- 通常コンポーネント
- Pending コンポーネント
- エラーコンポーネント
- Not Found コンポーネント
つまり、コンポーネントは遅延読み込みするべきだ、ということです。
詳しくは、以下のリンクを参照してください。
まとめ
今回は、TanStack Router の使い方について簡単にまとめてみました。
公式ドキュメントには、さらに多くの機能が提供されています。
- ファイルベースルーティング
- SSR
- ローダー
興味がある方は、公式ドキュメントを参照してみてください。
では、最後まで読んでいただきありがとうございました。
Discussion