Next.js 14/15の「全部Server Actionsでいい」論争に、現場のエンジニアが終止符を打つ

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だけでは体験が「重い」。
ここでTanStack Queryを導入するのは「過剰」ではない。UXを担保するための必要経費だ。

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 ActionsTanStack 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開発における最も合理的で、かつエンジニアの精神衛生を保てる選択肢だと確信している。

さあ、次はどんなコードを書く?