🌴

Next.jsはもう要らない?次世代フレームワークTanstack Startに入門してみた

に公開

TanStack Startとは

TanStack Startは、TanStack Routerをベースにしたフルスタックフレームワークだ。Vite上で動き、SSR・SSG・SPAすべてに対応する。React向けのフルスタック構成としてはNext.jsが定番だが、TanStack Startはまったく違うアプローチを取っている。

https://tanstack.com/start/latest

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