Next.js 14/15の「魔法の杖」を捨てろ:Server ActionsとReact Queryの正しい境界線

Next.js 14/15の「魔法の杖」を捨てろ:Server ActionsとReact Queryの正しい境界線

Next.jsのServer Actionsを「夢のオールインワン・ソリューション」だと勘違いしているなら、今すぐ目を覚ませ。

App Routerが登場して以来、「API Routeを作るのが面倒だから」という理由で、データ取得から更新まで全てをServer Actionsに詰め込む現場を嫌というほど見てきた。だが、現実は残酷だ。責務が曖昧になったフロントエンドは、コンポーネントのレンダリングライフサイクルとサーバーのシリアライズ制約に殺され、メンテナンス不可能なスパゲッティコードへと変貌する。

結論から言う。Server Actionsは「副作用(Mutation)」に専念させろ。データフェッチとキャッシュ管理は、React Query(TanStack Query)の聖域だ。


1. なぜ「全部Server Actions」が地獄への片道切符なのか

Server Actionsは確かに革命的だ。だが、これをデータフェッチに多用すると、「コンポーネントの責務」が崩壊する。

本来、UIコンポーネントは「表示」に集中すべきだ。しかし、Server Actionsでデータ取得を行うと、コンポーネント内でサーバーサイドの実行コンテキストとクライアントの状態が混ざり合い、以下の問題が噴出する。

  • レンダリングの不一致: サーバーコンポーネント経由のデータ取得と、クライアント側での動的更新のタイミングが食い違い、UXがガタつく。
  • キャッシュ制御の複雑化: `revalidatePath` を連打する設計は、スケールしない。特定のデータだけをピンポイントで更新したい場合、React Queryのような「クエリキーによる宣言的なキャッシュ管理」がないと、結局画面全体を再レンダリングする羽目になる。

2. 賢いエンジニアは「疎結合」を選ぶ

Server Actionsはあくまで「サーバーへのコマンド」であり、React Queryは「クライアントの状態管理エンジン」だ。これらを組み合わせることで、「サーバーサイドのストリーミングによる初期表示の高速化」と「クライアント側のインタラクティブな状態制御」を両立できる。

実践:Server Actions × React Query の最適解

// actions/user.ts
'use server'

import { z } from 'zod'

// 1. Zodでバリデーションを徹底し、DTO(Data Transfer Object)として型を確定させる
const UpdateUserSchema = z.object({
  name: z.string().min(1),
})

export async function updateUserName(data: unknown) {
  const parsed = UpdateUserSchema.parse(data)
  // データベース更新処理
  const user = await db.user.update({ data: { name: parsed.name } })
  
  // 戻り値はプリミティブなJSONに近い形にする(シリアライズの罠を回避)
  return { id: user.id, name: user.name }
}

// components/ProfileForm.tsx
'use client'

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updateUserName } from '@/actions/user'

export function ProfileForm() {
  const queryClient = useQueryClient()
  
  const { mutate } = useMutation({
    mutationFn: (name: string) => updateUserName({ name }),
    onSuccess: (updatedUser) => {
      // 楽観的更新の同期やキャッシュの無効化をここで行う
      queryClient.setQueryData(['user'], updatedUser)
    }
  })

  return <button onClick={() => mutate('新しい名前')}>更新</button>
}


3. 「シリアライズの呪い」と戦うための鉄則

Server Actionsは引数に「シリアライズ可能なデータ」しか受け付けない。`Date`型や複雑なクラスインスタンスをそのまま投げると、実行時に `DataCloneError` で爆死する。

  • 鉄則1: 複雑なオブジェクトは渡すな。プリミティブな値か、JSON互換の構造体(DTO)のみに絞る。
  • 鉄則2: 必ず `Zod` 等で入力値を検証する。サーバー側で型を再定義するのではなく、共有のバリデーションスキーマを持たせるのが現代の正攻法だ。

4. リアルタイム更新:幻想を追いかけるな

「Server ActionsがあればWebSocketは不要」という意見があるが、それは大規模アプリを知らない者の戯言だ。

Server Actionsはリクエスト・レスポンスの単発通信だ。これを無理やりポーリング(短時間でのリクエスト連打)でリアルタイムっぽく見せようとすると、サーバーのコネクションが枯渇し、Next.jsのキャッシュ戦略(ISRなど)と衝突してキャッシュが正しく機能しなくなる。

  • リアルタイムが必要なら: 素直に `Pusher` や `Supabase Realtime` を使え。
  • 状態同期で十分なら: React Queryの `staleTime` と `gcTime` を調整し、適切に再取得(Refetch)させるだけでUXは十分向上する。

5. 設計原則:統合のための黄金律

今後、Next.jsのバージョンが上がっても耐えうるコードを書くための原則を刻んでおけ。

責務ツール役割
副作用 (Mutation)Server ActionsDB更新、認証、メール送信などの「コマンド」
データ取得 (Query)React Queryキャッシュ管理、ローディング状態、楽観的更新
バリデーションZodServer Actionsの入り口での型安全確保

まとめ:設計のチェックリスト

1. Server Actionsは「薄く」保て: ロジックは別ファイル(Service層)に逃がし、Actionsはただのインターフェースにする。 2. 状態管理はReact Queryに託せ: サーバーから返ってきたデータは、即座にReact Queryのキャッシュに流し込む。 3. 副作用の副作用を考える: `revalidatePath` は最終手段。キャッシュのキーをうまく設計し、React Query側で制御できるようにする。

エンジニアリングとは、便利そうな道具を何でもかんでも使うことではない。「どこで線を引くか」を決めることだ。その境界線こそが、数年後の自分を助ける唯一の防波堤になる。