Reactの新しいルーティングライブラリ、TanStackRouterを学ぶ
Reactのルーティングには主にNextjs等のフレームワークやReact Routeが利用されます。この記事では新たなルーティング手法の選択肢としてTanStack Routerを紹介します。TanStack Routerでは最初に挙げた選択肢の使い心地を踏襲しつつ、ルーティングやサーチパラメータの型安全性や他にない便利な機能を提供します。
はじめに
React で開発する時、どのようにルーティングを実装していますか?Next.jsやRemixなどのフレームワークを用いて開発するときはフレームワークに実装されたルーティング利用し、フレームワークを利用しないときはReactRouterを利用する方法がスタンダードだと思います。 この記事では React におけるルーティング方法の新たな選択肢としてTanStack Routerを紹介します。名前から察する通り、これはTanStack Query(React Query)と同じ開発元のTanStackが開発したライブラリとなります。 この記事では紹介しませんが React だけではなく、Preact・Solid・Svelte・Vue・Angular でも利用できます。
特徴
TanStack Router は後発のライブラリということもあり、React Router のようなルーティング専用のライブラリや、Next.js のようなフレームワークに実装されたルーティングからインスピレーションを受けて構成されています。React Router や Next.js のルーティング機能と比較したい場合はこちらを確認してください。 そして、TanStack Router は大きく 3 つの特徴を持ちます。
型安全でシンプル
先述の通り ReactRouter や Next.js などの既存のルーティングを基に構築されたライブラリなので初めて使う人でも使いやすい設計になっています。また、100%の型安全性を保っており、Next.js の Statically Typed Links(執筆当時 beta 版の機能)のようなルーティングの型定義もされます。
優秀な検索パラメータ
検索パラメータを型安全にオブジェクトを扱うように変更や取得が可能でさらにバリデーションを行えます。検索パラメータを一種の状態管理を行うストアのように扱えるのでより直感的で安全に利用することが可能です。
クライアント側のデータ取得
TanStack Query や SWR などの stale-while-revalidate キャッシュを活かしたデータ取得ライブラリと相性が良い設計がなされています。
導入
パッケージをインストールするだけで TanStack Router を利用するための準備が完了します。
pnpm i @tanstack/router@beta
開発ツール
TanStack Router には専用の開発ツールが準備されており、TanStack Router の内部で行われいる振る舞いを可視化してくれます。
開発ツールは専用のパッケージをインストールして、コード内で埋め込むことで利用できます。
npm i -D @tanstack/router-devtools@beta
開発環境でのみ動作するコンポーネントとして定義して、そのコンポーネントを埋め込んだ場所にツールが表示されます。
const TanStackRouterDevtools = import.meta.env.DEV
? // 遅延読み込みする
lazy(() =>
import('@tanstack/router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
})),
)
: () => null;
一般的には TanStack Router のルートであるRootRoute
クラスを定義する箇所に埋め込みます。
const rootRoute = new RootRoute({
component: () => (
<>
<Outlet />
<TanStackRouterDevtools />
</>
),
});
このように配置した場合は

上記のようなアイコンが左下に常に表示され、クリックすることで開発ツールが開かれます。

ページごとの状況や動作を細かく確認できるのでとても便利です。
そのほかにもオプションを props として渡すことでアイコンの配置場所やデフォルトの振る舞いを調整できます。
使い方
ルーティングの作成
RootRoute
でルーティングの入り口を作ります。
const rootRoute = new RootRoute();
そしてRoute
で各ルートの設定を行います。
const indexRoute = new Route({
getParentRoot: () => rootRoute,
path: '/',
component: () => <IndexContent />,
});
getParentRoot
は作成するルートの親となるルートを指定します。そして、path
には指定した親からの相対パスを渡します。/blog/create
にルートを作るとします。
path
に/blog/create
を渡した時は親にrootRoute
を、path
が/create
であれば親に/blog
にルートを作るblogRoot
を指定します。path
として指定した文字列の先頭と末尾のスラッシュはいい感じに解決してくれます。例えば/blog/
というpath
を持つルートの子にpath
が/create
となるルートを追加するとそのルートは/blog//create
になりそうですが、/blog/create
に調整されます。
この 2 つがgetParentPath
とpath
がRoute
を定義するために必須のオプションです(厳密にはpath
は代わりにid
を入力しても良いです)。
必須のオプション以外の任意のオプションではcomponent
にそのルートにアクセスしたときに表示するコンポーネントを渡せます。その他にも表示するルートでエラーが発生した時に表示するerrorComponent
や、Promise の解決中に表示するpendingComponent
や、検索パラメータをバリデーションするvalidateSearch
など様々なものを渡せます。
使用するルートを準備できたら、改めてルーティング全体を組み上げていきます。親要素のルートに対してaddChildren
で子要素を配置するようにします。
const rootRoute = new RootRoute({});
// /
const indexRoute = new Route({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>Index</div>
});
// /blog
const blogRoute = new Route({
getParentRoute: () => rootRoute,
path: 'blog',
component: () => <div>Blog</div>
});
// /blog/:slug
const postRoute = new Route({
getParentRoute: () => blogRoute,
path: '$slug',
component: () => <div>Post</div>
});
// /accounts/:id
const userRoute = new Route({
getParentRoute: () => rootRoute,
path: 'accounts/$id',
// 遅延読み込みする場合に使うlazyはTanStack Routerが提供する関数
component: () => lazy(() => import('/Account'))
}).
const routeTree = rootRoute.addChildren([
indexRoute,
blogRoute.addChildren([postRoute]),
userRoute,
])
addChildren
とgetParentRoute
の両方でルーティングツリーを構成しているように見えます。重複した定義のように感じますが、これによって型安全なルーティングが構成されます。
組み上げたルーティングrouterTree
はさらにRouter
インスタンスを用いてルータを作成し、declaration mergingでプロジェクト全体にルーティングに関する型の情報を共有します。
const router = new Router({ routeTree });
declare module '@tanstack/router' {
interface Register {
router: typeof router;
}
}
ルーターはRouterProvider
コンポーネントを Provider を埋め込んでを React のツリーに反映します。
const App: FC = () => {
return <RouterProvider router={router} />;
};
ページの遷移
ページの遷移はコンポーネントとしてLink
とNavigate
、hooks としてはuseNavigate
、そしてrouter.navigate
が提供されています。
Link
Link
コンポーネントは他のライブラリと同じで、SPA で遷移するa
タグとしてレンダリングされます。
/blog
へ遷移するリンクを作りたい時は以下のように書きます。
import { Link } from '@tanstack/router';
const Link: FC = () => {
return <Link to="/blog">Blog</Link>;
};
このリンクは以下のような html を出力し、クリックなどのアクションで/blog
に SPA で遷移します。
<a href="/blog">Blog</a>
to
には作成したルーティングに存在するパスしか指定できません。これが TanStack Router の持つ特徴の型安全性の一つです。
/blog/slug
のようなルートへ動的に遷移させたい場合は他のフレームワークと異なり動的な分離して定義します。これによってto
の型は動的リンクで合っても厳密な指定を可能にしています。
import { Link } from '@tanstack/router';
const Link: FC = () => {
return (
<Link
to="/blog/$slug"
params={{
slug: 'tanstack-router',
}}
>
Blog
</Link>
);
};
他にも検索パラメータが必要な場合はsearch
で、ハッシュリンクを渡したいときはhash
で指定することができます。
例えば/blog?search=tanstack&sort=name#Search
にリンクしたい場合は以下のように書きます。
import { Link } from '@tanstack/router';
const Link: FC = () => {
return (
<Link
to="/blog"
search={{
search: 'tanstack',
sort: 'name',
}}
hash="Search"
>
Blog
</Link>
);
};
このように書くことで 遷移先のルートで登録されたバリデーションを行える点などにメリットがあります。バリデーションについては後ほど記述します。
さらに、preload
でリンクにホバーやタッチしたタイミングから遷移先の読み込みを行うように指定できます。
import { Link } from '@tanstack/router';
const Link: FC = () => {
return (
<Link to="/blog" preload="intent">
Blog
</Link>
);
};
他にもfrom
を起源としてリンク先を記述する相対リンクや、Link
の状態に応じたstyle
を指定できるactiveProps
などを指定することができます。
Navigate
Navigate
はコンポーネントがレンダリングされたら遷移を行うようコンポーネントです。使われる場面として考えられるのは、アカウントの設定画面に遷移したときにログインしていなければログイン画面へ遷移させるようなケースです。
const Setting: FC = () => {
const user = useUser();
if (!user) {
return (
<Navigate
to="/login"
search={{ redirect = '/accounts/setting' }}
/>
);
}
return <AccountSetting user={user} />;
};
このコンポーネントの Props として渡せる値はLink
とほとんど同じです。
useNavigate
navigate
という遷移に関する機能を持つインターフェイスを返す hooks です。ブログを作成するページで作成後にブログの詳細ページへ飛ばす挙動を作成するときなどで使われる考えています。
const navigate = useNavigate();
const onSubmit = async (data: CreateDTO) => {
const response = await trigger(data);
if (response.ok) {
navigate({ to: '/blog/$slug', params: { slug: data.slug } });
}
};
navigate
に渡す値はオブジェクト形式で、Link
と同じような値を渡せます(from
はuseNavigate
で渡せるなどの差異はあります)。
router.navigate
router.navigate
はRouter
インスタンスが持つメソッドで、useNavigate
が返すnavigate
と同じ能力を持ちます。hooks を用いたくないタイミングで有効的に活用できます。
const router = new Router({ routeTree });
router.navigate({ to: '/blog/$slug', params: { slug: data.slug } });
バリデーション
TanStack Router ではRoute
で検索パラメータのバリデーションや型情報の付与が可能です。
const blogRoute = new Route({
getParentRoute: () => rootRoute,
path: 'blog',
validateSearch: (search): BlogSearch => {
return {
page: Number(search?.page ?? 1),
filter: search.filter || '',
sort: search.sort || 'name',
};
},
});
// zodなどのライブラリを用いて検査する
const blogRoute = new Route({
getParentRoute: () => rootRoute,
path: 'blog',
validateSearch: (search): BlogSearch => {
return blogSearchSchema.parse(search);
},
});
このようにして検索パラメータに型を付与しつつ、入力値の検査を行うことができます。
useSearch
によってコンポーネント内で取得した検索パラメータの値を取得できますが、その値の型はBlogSearch
になります。他にも、このルート内で扱う検索パラメータの値の型は全てBlogSearch
になります。
このルートに対する遷移を考えてみます。
<Link to="/blog" search={{ page: 1, filter: 'blog' }}>
<Link to="/blog" search={{ page: false, filter: symbol(5) }}>
上記のように定義した 2 つのLink
のうち後者は検索パラメータの型が不一致のためエラーが発生します。
データの取得
TanStack Router ではルーティングに沿ったデータの取り扱いが可能です。Nextjs や remix が持つデータの保持やキャッシュ戦略はデータ取得ライブラリに任せて、取得のタイミングの調整のみを行います。
データの取得やキャッシュのために開発された TanStack Loader と組み合わせて使ったり、他にも TanStack Query や SWR、Recoil や apollo のような様々なライブラリと組み合わせて使えます。
TanStack Loader についてはサンプルで使われる程度の情報量しかなく情報を正確に取得できなかったので、この記事では TanStack Query を用いて紹介します。
ルーティングに合わせたデータ取得なのでルートごとに定義します。Route
の引数loader
でデータの取得についてのロジックを記述します。loader
に記述した関数はルートの移動や更新、preload
のたびに実行されます。
const postRoute = new Route({
getParentRoute: () => blogRoute,
path: '$slug',
loader: ({ params, search }) => {
return queryClient.ensureQueryData({
queryKey: ['blog', params.slug],
queryFn: () => fetchBlog(params.slug, search),
});
},
validateSearch: (search): BlogSearch => {
return blogSearchSchema.parse(search);
},
component: () => {
const { slug } = useParams({ from: '/blog/$slug' });
const search = useSearch({ from: '/blog/$slug' });
const blog = useQuery(['blog', slug], () =>
fetchPostById(slug, search),
);
return <Blog blog={blog} />;
},
});
この例ではcomponent
がレンダリングされる前にルートの移動、更新、preload
の時点で api を呼び出してキャッシュを更新しています。これによってページの読み込み後の非同期なデータをより早く解決することが可能になります。
preload
を有効にするには遷移のタイミングでpreload
にintent
を付与するか、Router
を作成するタイミングでルーター全体のデフォルト設定にする必要があります。
<Link to="/blog" preload="intent" />;
new Router({
routeTree,
defaultProps: 'intent',
});
これによってLink
にホバーやタッチしたタイミングから先行してデータの読み込みを実行できます。
おわりに
TanStack Router の基本的な機能を紹介しました。これまで利用してきたライブラリの使用感を保ったまま新たな機能が追加された使いやすいルーティングライブラリに感じました。特に、検索パラメータを簡単に型安全で状態のように扱える点が新鮮でそこに惹かれています。 フレームワークを利用しないアプリケーションを作るときは beta 版ではありますが、TanStack Router を使ってみてはいかがでしょうか。