こんにちは、DXビジネス開発部の只野です。
普段はOPTiM Taglet、OPTiM AI Cameraシリーズの開発に携わっています。
本記事は OPTiM TECH BLOG Advent Calendar 2025 Day 4 の記事です。
はじめに
自分が担当しているプロダクトの一つに、React + Vite構成のSPAがあり、ルーティングライブラリとしてTanStack Routerを採用しています。
TanStack Routerは型安全かつファイルベースルーティングが可能なライブラリで、React Routerと同じくルーティング専用であり、Next.jsのようなフルスタックフレームワークではありません。
しかし最近、TanStack Routerを基盤としたフルスタックフレームワーク「TanStack Start」がRCになったので、動向を追うためにさわってみました。
TanStack Startとは
TanStack Routerに追加してSSRなどのサーバーサイド機能が追加されているフレームワークです。ビルドツールにはViteを使用しています。おおよそNext.jsと同じような機能が提供されており、現時点での制約はReact Server Component未対応な点です。(近い将来対応予定みたいです)
主な特徴としては、TanStack Routerと同様に型安全で開発者体験を重視している点と、クライアントサイドファーストの設計思想を持つ点です。サーバーサイドを重視するNext.jsなどとは対照的なアプローチなのかなと思います。
TanStack Startの思想については以下記事が大変参考になりました。クライアントサイドを中心に据え、必要がある時にサーバーサイドに処理を委譲するという考え方です。
ルーティング
TanStack Routerを基盤としているため、その全機能を利用できます。TanStack Startのドキュメントでもルーティングについては基本的な内容のみとなっているため、TanStack Routerをさわったことがない方は困ったらTanStack Routerのドキュメントを見に行くのが良さそうです。
TanStack Startではファイルベースルーティングを採用しており、以下のような形でルートを定義します。
// src/routes/posts.tsx → /posts export const Route = createFileRoute('/posts')({ component: PostsPage, })
実行モデル
TanStack Startのデフォルトだと、特に明示的に制限しない限りクライアントサイドとサーバーサイド両方のバンドルで実行されるようです。
// クライアント/サーバー両環境で実行される function formatPrice(price: number) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(price) } // ルートローダーは両環境対応 export const Route = createFileRoute('/products')({ loader: async () => { // SSR時のサーバー側と、ナビゲーション時のクライアント側の両方で実行される const response = await fetch('/api/products') return response.json() }, })
注意点として、ロジックの種別に応じてサーバー専用・クライアント専用・両環境対応のいずれかを適切に選択する必要があります。詳細は公式ドキュメントのExecution Modelを参照してください。
Server Functions
Server Functionsを使うことで、任意のロジックを常にサーバーサイドで実行させることができます。createServerFn()で作成します。
import { createServerFn } from '@tanstack/react-start' const getUsers = createServerFn({ method: 'GET' }).handler(async () => { // ここは必ずサーバーで実行される const apiKey = process.env.API_SECRET const response = await fetch(`https://api.example.com/users?key=${apiKey}`) return response.json() })
Server Functionsの呼び出し
Server Functionsはloaderやコンポーネントなど、様々な場所から呼び出せます。
// Server Functionの定義 const getPosts = createServerFn({ method: 'GET' }).handler(async () => { return await db.posts.findMany() }) // loaderから呼び出す export const Route = createFileRoute('/posts')({ loader: () => getPosts(), }) // コンポーネントから呼び出す function PostList() { // useServerFnフックを組み合わせて使用する const fetchPosts = useServerFn(getPosts) const { data } = useQuery({ queryKey: ['posts'], queryFn: () => fetchPosts(), }) }
所感
実際に触ってみて、TanStack Routerユーザーとしては非常に入りやすいと感じました。ルーティング周りの知識がそのまま使えるので、新しく覚えることはサーバーサイド機能に集中できます。
戸惑ったところとしては実行モデルの部分です。同じコードがサーバーとクライアント両方で動くisomorphicな考え方に馴染みがなく、どこで何が実行されるのか最初はイメージがなかなか掴めませんでした。ただ、公式チュートリアルにあるシーケンス図を参照しながら手を動かしていくうちに、徐々に理解できるようになりました。
既にTanStack Routerを使っているプロジェクトであれば、ルーティング設定をそのまま活かせるため移行のハードルは比較的低いのかなと思います。SPAからサーバーサイド機能を段階的に取り入れたいケースには良い選択肢になりそうだと思いました。
参考
おわりに
当社ではモダンなフロントエンド技術を使ったプロダクト開発を行っています。一緒に技術的なチャレンジを楽しんでくれる方、ぜひご応募ください!