TanStack Routerを使って基本的な動作確認を行ってみました
TanStackという単語を聞いた時に最初に思い浮かべるのはTanStack Queryという人も多いかと思いますがTanStack Query以外にもTanStack Table, TanStack FormなどさまざまなライブラリがTanStackという名前で提供されています。2023年12月現在Beta版ですが以前から少し気になっていたTanStack Routerを動かして基本的なルーティングの設定やData Loadingなどどのように設定するか確認を行いました。
ドキュメントを参考に設定を行っています。現在Betaなので今後APIに変更が加わる可能性もあります。また基本動作を確認しただけなので本書を読んだだけでTanStack Routerを使いこなせるわけではありません。
目次
TanStack Routerとは
TanStack RouterはReact専用のルーティングライブラリでReactで複数ページで構成されたType Safeのアプリケーションを構築したい場合に利用することができます。
100%TypeSafe SupportなのでTypeScriptを利用することで効率よく開発を進めることができます。
環境の構築
Reactプロジェクトの作成
ReactのルーティングライブラリなのでReactのプロジェクトの作成を行い、その後TanStack Routerのインストールを行います。
“npm create vite@latest”コマンドでプロジェクトの作成を行います。プロジェクト名には任意の名前であるtanstack-router-practiseとし、frameworkにReact, VariantにTypeScriptを選択します。
% npm create vite@latest
✔ Project name: … tanstack-router-practise
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Scaffolding project in /Users/mac/Desktop/tanstack-router-practise...
Done. Now run:
cd tanstack-router-practise
npm install
npm run dev
コマンドが完了したらtanstack-router-practiseに移動してnpm installコマンドを実行します。
% npm install
TanStack Routerのインストール
Reactプロジェクトを作成後TanStack Routerのインストールを行います。
% npm install @tanstack/react-router@beta
package.jsonファイルを確認するとTanStack RouterはBetaであることがわかります。
{
"name": "tanstack-router-practise",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-router": "^0.0.1-beta.279",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
初めてのTanStack Routerの設定
main.tsxファイルにTanStack Routerを動作させるために最低限必要な設定を行います。
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
RouterProvider,
Router,
RootRoute,
} from '@tanstack/react-router';
const rootRoute = new RootRoute({
component: () => <h1>Hello TanStack Router</h1>,
});
const routeTree = rootRoute;
const router = new Router({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
設定後、”npm run dev”コマンドで開発サーバを起動するとブラウザ上に”Hello TanStack Router”が表示されます。
“Hello TanSTack Router”をブラウザ上に表示させることができたので記述したコードの各行でどのような設定を行なっているのか確認していきます。
最初にnew RootRoute()でRoot Routeを設定しています。Rootの名前の通りのルーティングの元になるルーティングで引数にオプションを設定することができcomponentプロパティにコンポーネントを設定することでブラウザ上に表示させたい内容を記述することができます。引数に何も設定しない場合には自動Outletコンポーネントが設定されます。Outletコンポーネントは後ほど確認します。
const rootRoute = new RootRoute({
component: () => <h1>Hello TanStack Router</h1>,
});
次にRoute Treeの設定を行っています。Treeという名前がついている通りルーティングは”/”をルートを中心にツリー構造を取ります。ルートの下に子ルートが繋がっていくことになります。現在の設定では子ルートはなくRoot Routeしかないためシンプルな形になっていますがここでアプリケーションを構成するすべてのルーティングを設定する必要があります。
const routeTree = rootRoute;
routeTree作成後にRouter classの引数にrouteTreeを指定しrouteインスタンスを作成します。
const router = new Router({ routeTree });
TypeSafeを実現するために必須なコードが下記でこの記述あるなしによってどのような変化が起こるのか後ほど確認します。
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
作成したrouterインスタンスはRouteProviderのrouter propsに設定しています。
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
基本的なルーティング設定
TanStack RouterはルーティングライブラリなのでRoot Routeを元にルーティングを追加して複数ページのアプリケーションを構築していきます。
新規ルーティングの追加
新たにルーティングを追加したい場合にはどのような設定が必要なのか確認していきます。
Root Routeはnew RootRouteでルーティングを作成していきましたが通常のルーティングはnew Routeで作成することができます。Routeの引数にはparentRouteオプションで親のルーティング、pathオプションでは追加したルーティングに対応するURL、componentオプションではブラウザ上に表示させるコンポーネントの設定を行います。
import {
RouterProvider,
Router,
Route,
RootRoute,
} from '@tanstack/react-router';
//略
const aboutRoute = new Route({
getParentRoute: () => rootRoute,
path: 'about',
component: function About() {
return <h2>About Page</h2>;
},
});
作成したルーティングは必ずRoute Treeに追加する必要があります。追加したaboutRouterはRoot Routeの子ルートになるので、addchildrenメソッドで追加します。Root Routeの下には複数のルーティングを設定することができるのでaddChildrendの引数には配列でルーティングを設定します。
const routeTree = rootRoute.addChildren([aboutRoute]);
ルーティングの追加後にブラザウから/aboutにアクセスを行います。エラーは何も表示されませんが追加した/aboutにブラウザからアクセスしても表示される内容は変わりません。
Outletコンポーネント
aboutRouteに設定したcomponentの内容を表示させるためにはaboutRouteの親ルートにあたるRoot RouteのcomponentにOutletコンポーネントを追加する必要があります。設定したOutletの場所にRoot Routeの子ルートのaboutRouteで設定したコンポーネントの内容が表示されます。
//略
import {
Outlet,
RouterProvider,
Router,
Route,
RootRoute,
} from '@tanstack/react-router';
const rootRoute = new RootRoute({
component: () => (
<>
<h1>Hello TanStack Router</h1>
<Outlet />
</>
),
});
//略
設定後に/aboutにアクセスするとOutlet部分に”About Page”が表示されることが確認できます。
複数のルーティングの追加
さらに別のルーティングを追加したい場合にはaboutRouteと同様の方法で行います。contactRouteを追加します。追加したルーティングは忘れずにrootTreeのaddChildrenの配列に追加を行います。
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
Outlet,
RouterProvider,
Router,
Route,
RootRoute,
} from '@tanstack/react-router';
const rootRoute = new RootRoute({
component: () => (
<>
<h1>Hello TanStack Router</h1>
<Outlet />
</>
),
});
const aboutRoute = new Route({
getParentRoute: () => rootRoute,
path: '/about',
component: function About() {
return <h2>About Page</h2>;
},
});
const contactRoute = new Route({
getParentRoute: () => rootRoute,
path: 'contact',
component: function Contact() {
return <h2>Contact Page</h2>;
},
});
const routeTree = rootRoute.addChildren([aboutRoute, contactRoute]);
const router = new Router({ routeTree });
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
/contactにアクセスすると”Contact Page”が表示されます。
Root Routeのオプションを設定しない場合
ここまでの設定ではRoot Routeにcomponentオプションを設定していますが設定していない場合にはどのように表示されるか確認します。
const rootRoute = new RootRoute();
componentを設定していないためブラウザ上に表示させる内容はないため”/”にアクセスすると何も表示されません。
/aboutにアクセスするとAbout Pageが表示されます。Root Routeのオプションに何も設定を行わない場合には自動でOutletが設定され子ルートの内容が表示されることが確認できました。
リンクの設定
“/”, “/about”, “/contact”ページを表示することができましたが各ページにアクセスするためには手動でブラウザのURLに入力する必要があります。ページ間の移動がクリックで行えるようにナビゲーションメニューを設定します。Root Routeのコンポーネントの内容は各ページで必ず表示され、レイアウトとして利用することができるのでRoot Routeのコンポーネントで設定を行います。
const rootRoute = new RootRoute({
component: () => (
<>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</ul>
<h1>Hello TanStack Router</h1>
<Outlet />
</>
),
});
ブラウザで確認すると各ページの上部にナビゲーションメニューが表示されます。クリックするとページを移動することができますがクリックするためにページが再読み込みされます。
ページの再読み込みではなくページ内の更新が必要な箇所のみページ移動で更新されスムーズなページ移動を実現するためにaタグからLinkタグに変更を行います。属性もhrefからtoに変更します。
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
Outlet,
RouterProvider,
Router,
Route,
RootRoute,
Link,
} from '@tanstack/react-router';
const rootRoute = new RootRoute({
component: () => (
<>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
<h1>Hello TanStack Router</h1>
<Outlet />
</>
),
});
//略
設定変更後はスムーズにページ移動を行うことができます。
Type Safeの確認
下記のコードを追加することでTypeSafeを実現できるという話をしました。
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
Linkコンポーネントを設定する際にtoに設定するURLを設定する際に上のコードがあることでエディターの自動補完により設定したルーティングのパスを誤って記述する必要がなくなります。また存在しないURLを設定した場合には存在しないルーティングを設定していることを教えくてれるメッセージが表示されます。
下記のコードを削除する自動補完が効かなくなるので誤ったURLを設定してもエラーが表示されることはありません。
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
indexページの設定
“/”にアクセスした時にはRoot Routeのcomponentオプションに設定した内容しか表示されません。”/”にアクセスした場合にRoot Routeに設定した内容以外にも”/”ページで表示させたい内容を表示させるためindexRouteを追加します。
indexRouteのpathには”/”を設定しています。あとはaboutRoute, contactRouteの設定と同じです。
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
component: function Index() {
return <h2>Home Page</h2>;
},
});
新たなルーティングを追加した場合にはRootTreeへの追加も忘れずに行います。
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute, contactRoute]);
設定後に”/”にアクセスするとindexRouteで設定した内容が表示されます。”Home Page”の文字列は”/”にアクセスした場合のみ表示されます。
存在しないページへのアクセス
RootTreeに設定していないページにアクセスした場合でもRoot Routeに設定したコンポーネントの内容が表示されます。例えば/testにアクセスした場合は下記のように表示されます。
存在しないURLにアクセスが場合にユーザに対してページが存在しないことを”404 Not Found”という文字列で表示する方法を確認します。ここでは”404 Not Found”としていますが表示する内容は自由に設定することができます。
ページの存在しないNot Foundのルーティングの設定にはnew Routeではなくnew NotFoundRouteを利用します。NotFoundRouteでもgetParentRoute, componentを設定することができますがNotFoundRouteで設定したRouteはRoute Treeに追加せず、Routerの引数のオプションとしてrouteTreeとは別に設定します。
//略
const notFoundRoute = new NotFoundRoute({
getParentRoute: () => rootRoute,
component: NotFound,
});
function NotFound() {
return <h2>404 - Not Found</h2>;
}
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute, contactRoute]);
const router = new Router({ routeTree, notFoundRoute });
//略
NotFoundRoute設定後に/testにアクセスするとNotFoundRouteのcomponentで設定した内容が表示されます。存在しないURLにアクセスがあった場合にはページが存在しないことをユーザに伝えることが可能になりました。
Data Loading
TanStack Routerではルーティングの設定の中でデータ取得を行うData Loadingの処理を記述することができます。
Loaderの設定
Data Loadingの動作確認を行うため新たに/postsのルーティングの設定を行います。Data Loadingの設定を行なっているのでnew Routeの引数にはこれまでに追加したルーティングの設定にはないloaderオプションを追加しています。
type Post = {
id: string;
title: string;
};
const fetchPosts = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts: Post[] = await res.json();
return posts;
};
const postsRoute = new Route({
getParentRoute: () => rootRoute,
path: 'posts',
loader: () => fetchPosts(),
component: PostsComponent,
});
postsRouteのcomponentで設定sちあPostsComponetではloader関数で取得したデータはuseLoaderDataメソッドを利用して取得することができます。
const PostsComponent = () => {
const posts = postsRoute.useLoaderData();
return (
lt;>
lt;h2>Postslt;/h2>
lt;ul>
{posts.map((post) => (
lt;li key={post.id}>
{post.id}:{post.title}
lt;/li>
))}
lt;/ul>
lt;/>
);
};
追加したルーティングはRoute Treeに設定する必要があります。
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
contactRoute,
postsRoute,
]);
ブラウザから/postsにアクセスすると100件分のPOSTデータが表示されます。loaderによってデータを取得することが確認できました。
Path Paramsの設定
これまでのルーティングでは/about, /contact, /postsのようにpathが変わらない設定を行ってきました。/posts/1, /posts/2のようにURLが動的に変わる場合のルーティングの設定について確認していきます。
動的に変わるPathについては”$”をつけます。例えば/posts/1, /posts/2のように/posts/の後ろの値が変わる場合は任意の名前postIdの名前に$をつけ$postIdとします。
新たに$postIdを持つpostRouteのルーティングを追加した場合は下記のように記述します。
const postRoute = new Route({
getParentRoute: () => postsRoute,
path: '$postId',
loader: ({ params }) => fetchPost(params.postId),
component: PostComponent,
});
postRouteはpostsRouteの子ルーティングとして設定を行うのでgertParentRouteではrootRouteではなくpostsRouteを設定しています。pathは$postIdを設定していますが実際のPathはgetParentRootで設定したpostsRouteのpathがpostなので/post/$postIdとなります。$postIdの値はloaderオプションの関数の引数paramsの中に入っているのでparams.postIdで取得することができます。
loaderオプションで指定したfetchPost関数はJSONPlaceHolderにアクセスを行いPostデータを取得します。
const fetchPost = async (postId: string) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
const post: Post = await res.json();
return post;
};
PostComponentではuseLoaderDataメソッドを利用してfetchPost関数で取得したデータを取得しています。
const PostComponent = () => {
const post = postRoute.useLoaderData();
return (
<>
<h2>Single Post</h2>
<div>
<p>ID:{post.id}</p>
<p>タイトル:{post.title}</p>
<p>内容:{post.body}</p>
</div>
</>
);
};
PostComponentからparamsのpostIdにアクセスしたい場合にはuseParamsメソッドを利用することができます。
ルーティングを追加した場合にはRoute Treeへの追加が必要になります。postRouteはpostsRouteの子ルートなのでaddChildrenを利用して設定を行います。
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
contactRoute,
postsRoute.addChildren([postRoute]),
]);
記事一覧を表示していたPostsComponentではLinkコンポーネントを利用して/posts/1, /posts/2へのリンクを設定します。PostComponentの内容を表示させるためにOutletコンポーネントも追加しています。
const PostsComponent = () => {
const posts = postsRoute.useLoaderData();
return (
<>
<h2>Posts</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link
to="/posts/$postId"
params={{
postId: post.id,
}}
>
{post.id}:{post.title}
</Link>
</li>
))}
</ul>
<Outlet />
</>
);
};
$postIdの自動補完も効いているのでtoの設定も間違いなく行うことができます。
これで/posts/1にアクセスした場合にはpostIdの1の個別のPostデータを表示することができますがPost一覧の下に表示されます。
Post一覧とPostデータのページを分けるためにPost一覧を表示させる新たなpostsIndexRouteのルーティングを追加します。postsIndexRouteのloaderオプションでPost一覧を取得します。
const postsIndexRoute = new Route({
getParentRoute: () => postsRoute,
path: '/',
loader: () => fetchPosts(),
component: PostsIndexComponent,
});
PostsIndexComponentを追加します。
const PostsIndexComponent = () => {
const posts = postsIndexRoute.useLoaderData();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link
to="/posts/$postId"
params={{
postId: post.id,
}}
>
{post.id}:{post.title}
</Link>
</li>
))}
</ul>
);
};
routeTreeに追加したルーティングを設定します。
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
contactRoute,
postsRoute.addChildren([postsIndexRoute, postRoute]),
]);
postsRoute、postsComponentも更新する必要があります。postsRouteからloaderオプションを削除します。
const postsRoute = new Route({
getParentRoute: () => rootRoute,
path: 'posts',
component: PostsComponent,
});
postsComponentではuseLoaderDataメソッドとPost一覧を表示するためのコードを削除します。
const PostsComponent = () => {
return (
<>
<h2>Posts</h2>
<Outlet />
</>
);
};
/postsにアクセスするとこれまで通りPost一覧が表示され、/posts/1にアクセスすると個別Postのみ表示されます。
動的に変わるURLのルーティングの設定方法を確認することができました。
Not Foundエラーの表示
Post一覧には100件分のデータが表示されるため/posts/1, /posts/2,…./posts/100までであれば正しく個別Postのページが表示されます。/posts/101にアクセスした場合には各項目に値が入っていない画面が表示されます。
fetch関数でリクエストに成功したか確認するためにfetchPost関数の中でres.okの値を確認し、res.okがfalse(リクエストに失敗)した場合にはエラーをthrowするように設定します。
const fetchPost = async (postId: string) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
if (!res.ok) {
throw Error('Not Found');
}
const post: Post = await res.json();
return post;
};
/posts/101にアクセスするとTanStack Routerが準備したエラー画面が表示されます。
TanStack Routerが準備したエラー画面ではなくカスタマイズしたエラーメッセージを表示するためにpostRouteにオプションのerrorComponentを設定します。
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
Outlet,
RouterProvider,
Router,
Route,
RootRoute,
Link,
NotFoundRoute,
ErrorRouteProps,
} from '@tanstack/react-router';
//略
const postRoute = new Route({
getParentRoute: () => postsRoute,
path: '$postId',
loader: ({ params }) => fetchPost(params.postId),
errorComponent: ({ error }: ErrorRouteProps) => {
if (error instanceof Error) {
return <div>{error.message}</div>;
}
},
component: PostComponent,
});
//略
errorComponentで設定した内容が表示されます。
Preloading
Preloadingの機能を利用することでユーザがページにアクセスする前にデータを取得しておくことが可能です。設定はRouterの引数にdefaultPreload: ‘intent’で行います。
const router = new Router({
routeTree,
notFoundRoute,
defaultPreload: 'intent',
});
設定後はリンクの上にカーソルを乗せるとページに移動する前にデータの取得が行われます。
利用したコード
本文書で動作確認を行なったコードの全体は下記の通りです。
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
Outlet,
RouterProvider,
Router,
Route,
RootRoute,
Link,
NotFoundRoute,
ErrorRouteProps,
} from '@tanstack/react-router';
const rootRoute = new RootRoute({
component: () => (
<>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
<li>
<Link to="/posts">Posts</Link>
</li>
</ul>
<h1>Hello TanStack Router</h1>
<Outlet />
</>
),
});
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
component: function Index() {
return <h2>Home Page</h2>;
},
});
const aboutRoute = new Route({
getParentRoute: () => rootRoute,
path: 'about',
component: function About() {
return <h2>About Page</h2>;
},
});
const contactRoute = new Route({
getParentRoute: () => rootRoute,
path: 'contact',
component: function Contact() {
return <h2>Contact</h2>;
},
});
type Post = {
id: string;
title: string;
body: string;
};
const fetchPosts = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const posts: Post[] = await res.json();
return posts;
};
const PostsComponent = () => {
return (
<>
<h2>Posts</h2>
<Outlet />
</>
);
};
const postsRoute = new Route({
getParentRoute: () => rootRoute,
path: 'posts',
component: PostsComponent,
});
const PostsIndexComponent = () => {
const posts = postsIndexRoute.useLoaderData();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link
to="/posts/$postId"
params={{
postId: post.id,
}}
>
{post.id}:{post.title}
</Link>
</li>
))}
</ul>
);
};
const postsIndexRoute = new Route({
getParentRoute: () => postsRoute,
path: '/',
loader: () => fetchPosts(),
component: PostsIndexComponent,
});
const fetchPost = async (postId: string) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
if (!res.ok) {
throw Error('Not Found');
}
const post: Post = await res.json();
return post;
};
const PostComponent = () => {
const post = postRoute.useLoaderData();
return (
<>
<h2>Single Post</h2>
<div>
<p>ID:{post.id}</p>
<p>タイトル:{post.title}</p>
<p>内容:{post.body}</p>
</div>
</>
);
};
const postRoute = new Route({
getParentRoute: () => postsRoute,
path: '$postId',
loader: ({ params }) => fetchPost(params.postId),
errorComponent: ({ error }: ErrorRouteProps) => {
if (error instanceof Error) {
return <div>{error.message}</div>;
}
},
component: PostComponent,
});
const notFoundRoute = new NotFoundRoute({
getParentRoute: () => rootRoute,
component: NotFound,
});
function NotFound() {
return <h2>404 - Not Found</h2>;
}
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
contactRoute,
postsRoute.addChildren([postsIndexRoute, postRoute]),
]);
const router = new Router({ routeTree, notFoundRoute });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);