
はじめに
UPSIDERのエンジニアの 太田 です。
新規プロダクト開発において、初めて実務でTanStack RouterとTanStack Startを使用する機会がありました。
本記事ではこれらのフレームワークを初めて使った際に戸惑った、TanStack Startのレンダリング方法と、TanStack RouterのbeforeLoadのふるまいについて書きます。
なお、本記事では以下のバージョンを使用しました。
@tanstack/react-router: 1.142.13 @tanstack/react-start: 1.142.13
TanStack RouterとTanStack Startの採用
私たちはフロントエンドのフレームワークにNext.jsを採用してきました。
またSSRやサーバーコンポーネントなどの必要がないと判断したプロダクトでは、Pages Routerで静的なexportを使用しています。
サーバーを持たないため機能が制限されますが、シンプルに配信できるメリットもあります。
しかしApp RouterやSSRを使用せずにNext.jsを使用し続けることに多少なりとも違和感を感じていたこと、TanStack RouterはSPAをベースとしたフレームワークでありその他にも魅力的な機能に関する情報を見聞きしていたこと、また技術的な挑戦も含め、新規プロダクトではTanStack RouterとTanStack Startを採用することにしました。
※ 正直に書くとTanStack Routerのプロジェクトを作成したらTanStack Startが自動的についてきたというのが正しいです。
※ 社内で少ないながらTanStack Routerを使用したプロジェクトがあったことも選定する理由になりました。
最初の印象
TanStack RouterとTanStack Startを使い始めると、Next.jsと同じReactのフレームワークであることから、あまり戸惑うことなく開発を進められました。
beforeLoad を始めとしたRouterの機能についても、試行錯誤しながらではありましたが、開発中は直感的で便利であると感じ使用しました。
SearchパラメータにZodで検証した結果の型などを導入できる点も、たしかに魅力的に感じました。(ここについても思うことがあるので、また紹介できる機会があればと思います)
しかしいざビルドしてデプロイしようとした際に戸惑いました。
ビルド
まずデフォルトの設定でTanStack Startのプロジェクトをビルドしてみると、サーバーを前提とした成果物が生成されることがわかります。
HTMLファイルはひとつも生成されません。
サーバーサイドで生成したHTMLを返却するということでしょう。
今回はクライアントのみで完結することを想定しているので、SPAモードの設定をします。
// vite.config.ts spa: { enabled: true }
するとHTMLファイルは _shell.html という1ファイルのみ生成されます。
Next.jsなどのページごとのプリレンダリングをベースとしたSPAとは違い、素のVueやReactのようなピュアなSPAであることに気づきます。
次に、ページごとにプリレンダリングすることを考えました。
Next.jsの代替と考えていたため、自然とプリレンダリングをすることを考えたのです。
// vite.config.ts spa: { enabled: true, }, prerender: { enabled: true }
無事にページごとにHTMLファイルが生成されるようになりました。
しかししばらく開発を進めていくと、Next.jsと異なることに気づきます。
- 動的ルーティングページのHTMLファイルが生成されない
- 開発時に使用したAPIモックのデータが成果物の中に入っている
本来は認証情報に基づき、ユーザーごとに異なるデータを取得するものです。
モックデータが入ってしまうのは、あきらかに想定と異なるふるまいでした。
これはデータ取得を担っていた beforeLoad が、prerender設定によってビルド時に実行されてしまったために起きていました。ビルド時に一度だけ走った結果、そのときのAPIレスポンス(=開発時のモックデータ)がHTMLに焼き込まれてしまったのです。この beforeLoad の実行タイミングについては後ほど詳しく整理します。
Next.jsのPages RouterとTanStack Startは設計が異なる
ここまでTanStack Routerの便利な機能の恩恵を受けつつ、Next.jsと同じように使うことを考えてきましたが、Next.jsのPages RouterとTanStack Startは設計が異なることに気づきました。
Next.jsはページごとのプリレンダリングを前提としたSPAフレームワークです。
シングルページアプリケーションといいつつ、出力されるHTMLはシングルページではありません。
一方TanStack Routerは、ページごとのプリレンダリングを前提としていないピュアなSPAフレームワークです。
TanStack Startにより、そのピュアなSPAにページごとのプリレンダリングを追加できるようになりました。
beforeLoadの実行
beforeLoad は、そのルートのコンポーネントがレンダリングされる前に実行される関数です。
認証チェックやリダイレクト、後続の処理で使うデータの取得など、ページを表示する前に済ませておきたい処理を書くのに便利です。
今回はここでAPIからデータを取得していました。
※ データ取得には loader という専用の関数もありますが、今回は触れません。
ページ表示前にデータ取得などを行う関数として、Next.jsのPages Routerには getServerSideProps と getStaticProps があります。
ただし getServerSideProps はリクエストごとにサーバーで実行される関数であり、ビルド時には実行されず、静的exportを設定している場合は使用できません。
getStaticProps はビルド時のみに実行されます。(ISR関連はここでは割愛します)
Next.jsのこれらの関数は、使える設定が限られているぶん、どの関数がいつ・どこで実行されるのかが明確です。名前を見れば「サーバーサイドで動く」「静的生成時に動く」とわかるようになっており、迷うことはあまりありませんでした。
一方 beforeLoad は、サーバーサイド・ビルド時・クライアントのいずれでも実行されうる関数です。ひとつの関数がすべての場面を担うぶん、名前だけからは「いつ・どこで実行されるのか」が読み取れません。
どこで実行されるかは、レンダリング設定によって変わります。
| 設定 | 実行される場所・タイミング |
|---|---|
| TanStack Startを使わない/SPA設定 | クライアント |
| SSR(初回アクセス) | サーバー |
| SSR(クライアントサイドルーティング) | クライアント |
| prerender | ビルド時のサーバー(アクセス時には実行されない) |
つまり同じ beforeLoad でも、SPAでは毎回クライアントで走るのに対し、prerenderではビルド時に一度だけ走り、その結果が固定されます。
先ほどのモックデータ混入も、この「ビルド時に一度だけ実行される」性質が原因でした。
余談ですが、prerenderの挙動についてAIに聞くと、誤った答えが返ってくることが多々ありました。
逆に、上記の内容をAIに伝えて確認してみたところ、「おそらく正しいと思うが、ドキュメントに記載がないため判断できない」という答えでした。
そうした事情もあり、実際の挙動での確認をおすすめします。
おわりに
今回はNext.jsからの移行という感覚でTanStack RouterとTanStack Startを使い始めましたが、ビルドやレンダリングの段階で「これまでと同じように動くはず」という前提が通用しないことに気づかされました。
戸惑いの原因を振り返ると、その多くはNext.jsのメンタルモデルをそのまま持ち込んでしまったことにありました。
Next.jsがページごとのプリレンダリングを前提としたフレームワークであるのに対し、TanStack RouterはあくまでピュアなSPAであり、TanStack Startがその上にレンダリングの選択肢を重ねていく。
この設計の違いを理解して初めて、SPAモードやprerenderのふるまい、そして環境によって実行タイミングが変わるbeforeLoadの挙動が腑に落ちました。
新しいフレームワークに触れるときは既存の知識にあてはめて理解しようとしがちですが、今回のように前提そのものが異なる場合、その姿勢がかえって理解を妨げることもあると学びました。
そのフレームワークが何を前提に設計されているのかを一度立ち止まって捉え直すことの大切さを、あらためて感じています。
一方で、戸惑いこそあったものの、Routerの機能自体は開発体験として非常に魅力的でした。特にZodと組み合わせたSearchパラメータの型付けなど、まだ紹介しきれていない便利な点も多くあります。そのあたりはまた別の機会に書ければと思います。
また今回の経験を通して、あらためてNext.jsのPages Routerの良さにも気づきました。
プリレンダリングが前提として組み込まれているぶん、レンダリング方法をあまり意識せずとも静的に配信できる手軽さは、たしかに大きな利点だったのだと感じます。
同じようにNext.jsからTanStack Router / Startに触れる方にとって、この記事が最初の戸惑いを少しでも減らす一助になれば幸いです。
We Are Hiring
UPSIDER Engineering Deckはこちら📣