k8o

Reactの新しいルーティングライブラリ、TanStackRouterを学ぶ

公開: 2023年7月13日(木)
更新: 2024年9月28日(土)
434 views

Reactのルーティングには主にNextjs等のフレームワークやReact Routeが利用されます。この記事では新たなルーティング手法の選択肢としてTanStack Routerを紹介します。TanStack Routerでは最初に挙げた選択肢の使い心地を踏襲しつつ、ルーティングやサーチパラメータの型安全性や他にない便利な機能を提供します。

TypeScriptReactTanStack Router

はじめに

React で開発する時、どのようにルーティングを実装していますか?Next.jsRemixなどのフレームワークを用いて開発するときはフレームワークに実装されたルーティング利用し、フレームワークを利用しないときは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 />
    </>
  ),
});

このように配置した場合は

大文字の黒色でTANSTACK、改行して大文字で緑色のグラデーションでROUTERと書かれたアイコン

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

TanStackRouterの開発ツール 3つの部分に分かれていて特定のメソッドの一覧やマッチしたルート、サーチパラメータなどが記述されている

ページごとの状況や動作を細かく確認できるのでとても便利です。

そのほかにもオプションを 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 つがgetParentPathpathRouteを定義するために必須のオプションです(厳密には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,
])

addChildrengetParentRouteの両方でルーティングツリーを構成しているように見えます。重複した定義のように感じますが、これによって型安全なルーティングが構成されます。 組み上げたルーティング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} />;
};

ページの遷移

ページの遷移はコンポーネントとしてLinkNavigate、hooks としてはuseNavigate、そしてrouter.navigateが提供されています。

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はコンポーネントがレンダリングされたら遷移を行うようコンポーネントです。使われる場面として考えられるのは、アカウントの設定画面に遷移したときにログインしていなければログイン画面へ遷移させるようなケースです。

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と同じような値を渡せます(fromuseNavigateで渡せるなどの差異はあります)。

router.navigate

router.navigateRouterインスタンスが持つメソッドで、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を有効にするには遷移のタイミングでpreloadintentを付与するか、Routerを作成するタイミングでルーター全体のデフォルト設定にする必要があります。

<Link to="/blog" preload="intent" />;
 
new Router({
  routeTree,
  defaultProps: 'intent',
});

これによってLinkにホバーやタッチしたタイミングから先行してデータの読み込みを実行できます。

おわりに

TanStack Router の基本的な機能を紹介しました。これまで利用してきたライブラリの使用感を保ったまま新たな機能が追加された使いやすいルーティングライブラリに感じました。特に、検索パラメータを簡単に型安全で状態のように扱える点が新鮮でそこに惹かれています。 フレームワークを利用しないアプリケーションを作るときは beta 版ではありますが、TanStack Router を使ってみてはいかがでしょうか。