Next.jsはもう要らない?次世代フレームワークTanstack Startに入門してみた
TanStack Startとは
TanStack Startは、TanStack Routerをベースにしたフルスタックフレームワークだ。Vite上で動き、SSR・SSG・SPAすべてに対応する。React向けのフルスタック構成としてはNext.jsが定番だが、TanStack Startはまったく違うアプローチを取っている。
Next.jsとの一番の違いは思想だ。
- Next.js = オートマ車。乗れば走る。でもエンジンの中身は見えない
- TanStack Start = マニュアル車。全部自分で操作する。だからこそ仕組みがわかる
まず全体像を掴むために、主要ファイルを並べてみる。
| Tanstack Start | 項目 | Next.js |
|---|---|---|
__root.tsx |
レイアウト + HTML shell | layout.tsx |
routes/index.tsx |
ページ | app/page.tsx |
routeTree.gen.ts |
ルートの型を自動生成 | 無い |
router.tsx |
ルーター設定を明示 | 無い |
注目すべきはNext.jsに無い2つのファイルだ。
-
routeTree.gen.ts— ファイルを追加するたびに自動更新される。有効なルートがTypeScriptの型として生成される
この「暗黙 vs 明示」の違いが、これから紹介する3つの驚きに全部繋がっている。
驚き①: 型安全ルーティング — タイポが型エラーになる
一番興味深かったのがこれだ。
// routeTree.gen.ts(自動生成される)
export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
"/about": typeof AboutRoute;
"/todos": typeof TodosRoute;
}
つまりこうなる。
// ✅ 型チェックOK
<Link to="/todos">TODOアプリ</Link>
// ❌ 型エラー! "/abuot" は有効なパスじゃない
<Link to="/abuot">About</Link>
驚き②: loaderの型伝播 — 入口で型をつければ最後まで繋がる
const getPosts = createServerFn({ method: "GET" }).handler(async () => {
const res = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=10",
});
export const Route = createFileRoute("/posts")({
loader: () => getPosts(),
component: PostsPage,
});
function PostsPage() {
const posts = Route.useLoaderData(); // ← Post[] が自動で伝播する!
return posts.map((post) => <div key={post.id}>{post.title}</div>);
}
Next.jsだとこうなる。見覚えのある人も多いんじゃないだろうか。
// Server Component (page.tsx)
async function PostsPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = (await res.json()) as Post[]; // ← 1回目
return <PostList posts={posts} />;
}
// Client Component — 検索やソートなどインタラクティブな処理があるとこうなる
"use client";
function PostList({ posts }: { posts: Post[] }) {
// ← 2回目(propsの型定義)
const [query, setQuery] = useState("");
const filtered = posts.filter((p) => p.title.includes(query));
return filtered.map((post) => <div key={post.id}>{post.title}</div>);
}
TODOアプリを作ったときのコードがこれだ。
const addTodo = createServerFn({ method: "POST" })
return data;
})
.handler(async ({ data }) => {
todos.push({ id: nextId++, text: data.text, completed: false });
});
ミューテーション後の再取得
データを変更したあと、画面を更新するにはどうするか。
const router = useRouter();
const handleAdd = async () => {
await addTodo({ data: { text: newTodo } });
router.invalidate(); // ← 「loaderのキャッシュが古くなったから再取得して」
};
router.invalidate()で、クライアント側からloaderの再実行を指示する。Next.jsのrevalidatePath()はServer Action内からサーバー側のキャッシュを無効化し、結果的にクライアント側にも反映される。
アプローチが違うだけで結果は同じだが、TanStack Startの方が「クライアントからloaderを叩き直す」という挙動が直感的でわかりやすいと感じた。
正直な感想
良いことばかり書いてきたが、正直なところも書いておく。
コード量は確実に増える。
明示的にやる = 書く量が多い。Next.jsなら裏でやってくれることを全部自分で書くので、同じTODOアプリでもコードが長くなる。
初心者にはきつい。
ルーター設定、loaderの実行タイミング、createServerFnの必要性…。これらはNext.jsで一通り経験した人なら「あーこれを自分で書くのか」と理解できるが、フレームワーク初体験の人には辛い。
でも、「使ってるけど中身わかってない」が全部可視化される。
これが一番の価値だと思う。Next.jsのキャッシュ、ルーティング、サーバー実行が裏で何をやってるか、TanStack Startを触ると手触りで理解できる。
個人開発で好き放題やりたい人にはかなり良い。
フレームワークの「お作法」に縛られず、全部自分でコントロールできる。ルーター設定もキャッシュ戦略も自分で決められるので、「なんか勝手にこうなる」がない。
まとめ: Next.jsは要らない?
タイトルで煽っておいてなんだが、Next.jsは要らないとは思わない。
自分の結論はこうだ。
- 不特定多数に見せるプロダクト(SEO・OGP必要)→ Next.js
- ログイン後のみのアプリ → Vite + Reactで十分
- 仕組みを理解してフルコントロールしたい → TanStack Start
仕事ではNext.jsが安泰だと思う。エコシステム、ドキュメント、採用事例、どれを取っても圧倒的だ。
ただ、マニュアル車に乗る経験は、オートマ車の運転も上手くする。Next.jsの裏側で何が起きてるか知りたい人は、一度TanStack Startを触ってみてほしい。
Discussion