Next.js 14/15の「全部Server Actionsでいい」論争に、現場のエンジニアが終止符を打つ
「Next.jsのServer Actionsがあるんだから、TanStack Queryなんて要らないだろ」。最近、この手の議論を耳にするたびに少しだけ溜息が出る。
結論から言おう。Server Actionsは「魔法の杖」じゃない。
確かに、APIエンドポイントを量産する時代は終わった。だが、Server Actionsを「状態管理ライブラリ」の代わりとして使い、`useState`と`useEffect`で泥臭くデータを同期させようとするのは、数年前の「Reduxで全部管理しようぜ」という過ちを繰り返しているに過ぎない。
この記事では、僕が現場の泥沼で得た「Server ActionsとTanStack Queryの正しい棲み分け」を共有する。
1. なぜ「Server Componentsで取得すればいい」だけでは足りないのか
Next.jsのApp Routerは、確かに`fetch`のキャッシュAPIで強力なデータ取得を実現できる。静的なページならそれで十分だ。しかし、ユーザーの操作に応じて画面が動的に変化する「アプリケーション」を作ろうとした瞬間、状況は一変する。
- Server Componentsの限界: サーバー側で完結するデータ取得は、あくまで「初期表示」や「ナビゲーション」に最適化されている。
- クライアントの状態管理: ユーザーが検索窓を叩いた時、ソートを変更した時、あるいはOptimistic UI(楽観的更新)でUIを即座に反応させたい時、Server Componentsだけでは体験が「重い」。
2. 実践的アーキテクチャ:書き込みは「Actions」、読み込みは「Query」
僕の現在の最適解は、「副作用を伴うものはServer Actions、それ以外はTanStack Query」というシンプルな原則だ。
書き込みの要:Server Actions
Server Actionsは「データベースへの書き込み」に特化させる。ここで重要なのは、DTO(Data Transfer Object)への変換とバリデーションだ。'use server'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { revalidateTag } from 'next/cache'
// Zodで入力を厳格に制御。型安全をサーバー境界で担保する
const TodoSchema = z.object({
title: z.string().min(1).max(100),
})
export async function addTodo(data: unknown) {
const parsed = TodoSchema.safeParse(data)
if (!parsed.success) throw new Error('Invalid input')
const todo = await prisma.todo.create({
data: parsed.data
})
// キャッシュを無効化し、データの一貫性を保つ
revalidateTag('todos')
return todo
}
読み込みの要:TanStack Query
クライアント側では、React Queryを使って「サーバー状態」をキャッシュする。これにより、ブラウザの戻るボタンや再フォーカス時に、余計なネットワークリクエストを飛ばさずに済む。import { useMutation, useQueryClient } from '@tanstack/react-query'
import { addTodo } from '@/actions/todo'
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: addTodo,
onSuccess: () => {
// データの不整合を防ぐため、関連するクエリを強制的に再フェッチ
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
onMutate: async (newTodo) => {
// 楽観的更新:レスポンスを待たずにUIを先行更新する
await queryClient.cancelQueries({ queryKey: ['todos'] })
// ...ここにUI更新ロジック
}
})
}
3. シリアライズの罠を回避する
実務で一番ハマるのが、サーバーからクライアントへデータを渡す際の「シリアライズエラー」だ。ORMのモデルをそのまま返すと、`Date`型や`BigInt`型で必ず死ぬ。
解決策は明確だ。
1. DTOを使う: `prisma.todo.findMany()`の結果を、プリミティブな型のみを持つオブジェクトに変換する。
2. Superjsonの検討: どうしても複雑な型を渡したいなら`superjson`を入れるのも手だが、基本は「JSONとして安全なデータのみを返す」のが最も堅牢だ。
4. 結局、どこで使い分けるべきか?
「全部Server Actions」が適しているケースと、TanStack Queryを入れるべき境界線を整理した。
| 特徴 | Server Actions | TanStack Query |
|---|---|---|
| 主な用途 | フォーム送信、データ更新、認証 | 一覧取得、詳細表示、フィルタリング |
| 状態の寿命 | リクエスト単位(短命) | キャッシュ期間中(長命) |
| UXの強み | セキュリティと型の共有 | 楽観的更新、キャッシュ管理、再試行 |
| 導入の判断 | 必須(Next.jsの基本) | ユーザーが頻繁に操作する画面なら必須 |
TanStack Queryが「過剰」になる境界線
もし、あなたが作っているアプリが「管理画面の単一レコード編集」や「静的なブログ」であれば、TanStack Queryは不要だ。Server Actionsと`useActionState`(Next.js 15の新機能)だけで十分なUXが提供できる。逆に、「タスク管理アプリ」や「SNSのフィード」のように、ユーザーがボタンを連打し、リアルタイム性が求められるプロダクトなら、TanStack Queryを避けるのはエンジニアとしての怠慢だ。
最後に:ツールを盲信するな
技術スタックは宗教じゃない。Server ActionsはNext.jsの強力な武器だが、それを「唯一の解決策」と信じ込むのは、視界を狭める行為だ。
僕たちが作るべきなのは、コードの綺麗さ自慢のためのアーキテクチャじゃない。ユーザーが触った瞬間に「速い」と感じる、ストレスのない体験だ。
Server Actionsで書き込み、TanStack Queryで読み込む。この泥臭い組み合わせこそが、現代のNext.js開発における最も合理的で、かつエンジニアの精神衛生を保てる選択肢だと確信している。
さあ、次はどんなコードを書く?