20260725 101310 next.js 14 15におけるserver actionsとtanstack queryの使い分けと最適化

Next.js 14/15の「Server Actions至上主義」が陥る罠と、TanStack Queryによる解法

「全部Server Actionsで書けばシンプルになる」――そう信じていた時期が僕にもありました。App Routerの登場以降、API Routeを捨ててServer Actionsに全振りした結果、複雑なダッシュボードを前にして「画面の更新が追いつかない」「キャッシュの整合性が取れない」という地獄を見たエンジニアは多いはずです。

結論から言います。Server Actionsは「更新のトリガー」に徹し、クライアントの状態管理はTanStack Queryに任せる。 この境界線を曖昧にすると、アプリは確実に破綻します。


1. なぜ「Server Actions単体」ではUXが破綻するのか

Next.jsの`revalidatePath`や`revalidateTag`は強力ですが、これらはあくまで「サーバー側のキャッシュ」を無効化する機能です。一方で、フロントエンドの状態管理(React Queryのキャッシュ)は別のレイヤーで動いています。

#### 発生する「二重管理」のメカニズム
1. Server Action実行: `revalidatePath`でサーバー側のData Cacheを更新。
2. クライアントの乖離: ブラウザ側は「古いキャッシュ」を握ったまま。
3. UXの劣化: ユーザーが操作してもUIが更新されず、リロードして初めて反映される。

この乖離を埋めるために`useEffect`で無理やり再取得を走らせるようなコードは、保守性の悪夢です。

レイヤー役割キャッシュ管理
Server ActionsDB更新・副作用の実行Next.js Data Cache (revalidate)
TanStack Queryデータの取得・同期Client-side Cache (invalidate)

2. 【最適解】Server Actions × TanStack Query の連携パターン

理想的な構成は、Server Actionsで更新を行い、その直後にTanStack Queryのキャッシュを明示的に無効化することです。

'use client'

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updateUserData } from './actions'

export function ProfileForm() {
  const queryClient = useQueryClient()
  
  const { mutate } = useMutation({
    mutationFn: updateUserData,
    onSuccess: () => {
      // サーバー側の再検証とクライアントのキャッシュ無効化を同期させる
      queryClient.invalidateQueries({ queryKey: ['user'] })
    }
  })

  return <form action={(formData) => mutate(formData)}>...</form>
}

#### 楽観的更新(Optimistic Updates)で体感速度を極める
ユーザーは「ボタンを押した瞬間」にUIが変わることを期待しています。サーバーの応答を待つ必要はありません。

onMutate: async (newData) => {
  // 進行中のフェッチをキャンセルして競合を防ぐ(必須)
  await queryClient.cancelQueries({ queryKey: ['user'] })
  
  const previousData = queryClient.getQueryData(['user'])
  
  // UIを即座に更新
  queryClient.setQueryData(['user'], newData)
  
  return { previousData }
},
onError: (err, _, context) => {
  // 失敗時は元の状態にロールバック
  queryClient.setQueryData(['user'], context?.previousData)
}

`cancelQueries`を呼ぶ理由は、バックグラウンドでの再フェッチと手動の更新が競合し、UIが一瞬チラつくのを防ぐためです。


3. App Router時代の肝:Hydrationパターンの採用

初期表示の高速化を諦めてはいけません。Server Componentsで取得したデータを、クライアント側でTanStack Queryの初期値として引き継ぐ(Hydration)のがプロの作法です。

// Server Component
async function Page() {
  const data = await getUserData()
  const queryClient = new QueryClient()
  
  await queryClient.prefetchQuery({ queryKey: ['user'], queryFn: () => data })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ProfileClientComponent />
    </HydrationBoundary>
  )
}

これにより、SSRでHTMLを生成しつつ、クライアント側ではTanStack Queryがそのデータを「キャッシュ済み」として引き継ぐため、「初期表示は爆速、インタラクションもサクサク」という理想のUXが完成します。


4. Next.js 14/15での使い分け:`useActionState`の導入

Next.js 15(React 19)では、`useFormState`が`useActionState`へと進化しました。フォーム送信時の状態管理がより直感的になっています。

// Next.js 15以降の推奨パターン
const [state, formAction, isPending] = useActionState(
  updateUserData, 
  initialState
)

結論:

  • データ取得: `Server Components` + `TanStack Query` (Hydration)

  • データ更新: `Server Actions` + `TanStack Query` (Mutation + Invalidation)


この役割分担を徹底することで、Next.js特有のキャッシュの複雑さから解放されます。

深夜2時、コードが綺麗に整理された瞬間の静寂は格別です。Server Actionsを「魔法の杖」だと思わず、「状態管理の司令塔」としてのTanStack Queryと組み合わせる。この二刀流こそが、今のApp Routerで生き残るための唯一の道だと確信しています。