Next.js 14/15で「状態管理」を捨てろ:Server Actionsがもたらすアーキテクチャの革命
「またReact Queryを入れるのか?」
プロジェクトのセットアップ時、脳死で`npm install @tanstack/react-query`を叩くその指を一度止めてほしい。かつてSPA全盛期に我々が必死に構築した「クライアントサイドでのデータ同期」という複雑怪奇な城は、Next.jsのApp Routerという新しい地平において、もはや負債でしかない。
クライアントで状態を管理し、キャッシュをinvalidateし、ローディング状態を追いかける。そんな苦行から解放される時が来た。本稿では、Server ActionsとRSC(React Server Components)を軸にした、現代的なデータフローの設計思想を共有する。
1. パラダイムシフト:Client-side FetchingからServer-side Mutationへ
従来のReact開発では、「データ取得はクライアントで行うもの」という前提があった。しかし、Next.jsのApp Routerは「データはサーバーで取得し、アクションはサーバーで完結させる」という原則に回帰している。
なぜReact Queryが不要になるのか。それは、クライアントの状態管理が担っていた「サーバーとの同期」という責務を、Next.jsのData CacheとRouter Cacheがネイティブで肩代わりしてくれるからだ。
キャッシュの階層構造を理解する
多くのエンジニアが躓くのは、Next.jsが持つ二つのキャッシュ層の混同だ。
- Data Cache: サーバーサイドの`fetch`結果を永続化する。`revalidatePath`や`revalidateTag`でパージされる。
- Router Cache: ブラウザ側のメモリ上でセグメントごとのRSCペイロードを保持する。
2. 失敗から学ぶ:`revalidatePath`の罠とメカニズム
かつて私も「削除処理を叩いたのにUIが変わらない」という事態に半日を溶かしたことがある。原因は単純、パス指定の粒度だ。
`revalidatePath('/dashboard')` と書いたとき、Next.jsは `/dashboard` セグメントを再検証する。しかし、もし対象が `/dashboard/items/[id]` のような動的ルート内にあり、キャッシュタグの設計が甘ければ、親のキャッシュはパージされても、末端のセグメントが古いまま残ることがある。
教訓: パス指定に迷ったら `revalidateTag` を使え。
// actions/delete-item.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function deleteItem(id: string) {
await db.item.delete({ where: { id } })
// パスに依存せず、タグ単位でキャッシュを無効化する
revalidateTag('items-list')
}
3. 実装:Zodによる堅牢なServer Actions
Server Actionsはただの関数だ。だからこそ、境界線(Boundary)でのバリデーションが命綱になる。`zod`を組み合わせ、型安全なインターフェースを構築するユーティリティを作成しよう。
// lib/safe-action.ts
import { z } from 'zod';
export async function createSafeAction<T extends z.ZodSchema, R>(
schema: T,
action: (data: z.infer<T>) => Promise<R>
) {
return async (formData: FormData | z.infer<T>) => {
const data = formData instanceof FormData ? Object.fromEntries(formData) : formData;
const result = schema.safeParse(data);
if (!result.success) throw new Error('Invalid input');
return action(result.data);
};
}
これを使えば、サーバー側で予期せぬデータに爆死することはない。
4. リアルタイム性の正解:`useOptimistic`によるUXの最適化
「React Queryがないと楽観的更新(Optimistic UI)ができないのでは?」という懸念はもっともだ。しかし、React 18以降の `useOptimistic` を使えば、ライブラリなしで驚くほど滑らかに実装できる。
'use client'
import { useOptimistic, useTransition } from 'react'
export function ItemList({ initialItems }) {
const [items, addOptimisticItem] = useOptimistic(initialItems, (state, newItem) => [...state, newItem])
const [isPending, startTransition] = useTransition()
return (
<ul>
{items.map(item => <li key={item.id}>{item.title}</li>)}
<button onClick={() => startTransition(async () => {
addOptimisticItem({ id: 'temp', title: '追加中...' })
await addItemAction(...)
})}>追加</button>
</ul>
)
}
React Queryが提供していた複雑な状態管理ではなく、「遷移中の状態」をUIの副次的な表現として扱う。これがApp Router流の最適解だ。
結論:持続可能なアーキテクチャに向けて
Server ActionsとRSCを使いこなすことは、単なる「技術トレンドの追従」ではない。クライアントとサーバーの境界を再定義し、「ネットワーク越しに状態を同期する」という最もコストのかかる処理を、フレームワークのキャッシュ機構にオフロードするという高度なエンジニアリングだ。
今後、Partial Prerendering (PPR) が標準化されれば、静的なUIと動的なデータ取得の境界はさらに曖昧になる。その時、クライアントで必死にキャッシュを管理するコードは、完全に時代遅れの遺物となるだろう。
「何でもかんでもライブラリで解決する」という思考を捨てよう。サーバーに任せられることはサーバーにやらせる。その潔さこそが、半年後の君を救う、最もメンテナンス性の高いコードベースへの近道だ。