「Server ActionsがあればTanStack Queryは不要」。この言説を耳にするたび、私は冷や汗が出る。結論から言えば、その設計は「破綻」への片道切符だ。
Next.js 14/15でApp Routerが成熟し、確かに開発体験は劇的に向上した。だが、便利さの裏側で「クライアント側の状態管理」という、エンジニアが本来向き合うべき泥臭い課題が隠蔽されていることに気づいていない人が多すぎる。
なぜ、Server Actionsだけでは大規模なプロダクトを支えられないのか。実戦での痛い失敗から学んだ、生存戦略を共有する。
1. 「魔法の杖」がもたらすUXの崩壊
Server Actionsは強力だ。だが、それはあくまで「サーバーサイドの関数を呼ぶ」という一点においてのみ優れている。// 一見便利だが、ここには「UXの地獄」が潜んでいる
'use server'
export async function updateUserName(newName: string) {
await db.user.update({ ... })
revalidatePath('/profile')
}
このコードをボタン一つで叩くとしよう。ユーザーがボタンを押した瞬間、ページ全体が再レンダリングされるまで、画面は「無」になる。ネットワークが遅い環境なら、ユーザーは自分の操作が成功したのかすら分からず、連打するだろう。
ここでReact 18の`useOptimistic`を持ち出すのは、解決策としては正しいが、実装コストが見合わない。
// 複雑な状態管理のボイラープレート化
const [optimisticName, addOptimisticName] = useOptimistic(name);
const action = async (formData: FormData) => {
addOptimisticName(formData.get("name"));
await updateUserName(formData);
};
フォームが一つならいい。しかし、ダッシュボードのように複数のデータが相互に関連し合うUIで、これらをすべて自前管理しようとすれば、コードは`useState`のスパゲッティと化す。「宣言的にデータを同期させる」というReactの哲学を捨て、命令的な状態更新に追われる日々が待っているだけだ。
2. なぜ「再検証(Revalidation)」で詰むのか
私がかつて犯した最大の過ちは、`revalidatePath`を信頼しすぎたことだ。`revalidatePath`は、指定したパスのキャッシュをすべて破棄する。これは「全リソースの再取得」を意味する。小規模なアプリならいい。しかし、コンポーネントが複雑化し、APIリクエストが頻発する環境でこれを叩けば、サーバーの負荷は跳ね上がり、ユーザーのブラウザは不必要なレンダリングにリソースを食いつぶす。
| 機能 | Server Actions (revalidatePath) | TanStack Query (invalidateQueries) |
|---|---|---|
| 粒度 | パス全体(粗い) | クエリキー単位(非常に細かい) |
| コスト | 高(全コンポーネント再取得) | 低(必要分のみ再取得) |
| UX | ページ遷移に近い | データ同期(シームレス) |
TanStack Queryの`invalidateQueries`であれば、特定のユーザー情報だけ、あるいはリストの特定のページだけをピンポイントで再取得できる。この「制御の解像度」こそが、プロダクション環境でユーザーを離脱させないための必須条件だ。
3. 設計の指針:使い分けの決定版
Server Actionsは「データの書き込み(Mutation)」専用のツールと割り切るべきだ。一方で、読み取りと同期はTanStack Queryに任せる。#### どちらを使うべきか?(意思決定フロー)
1. その処理は「書き込み」か?
* Yes → Server Actions
* No → 次へ
2. UIに「読み込み中」や「楽観的更新」が必要か?
* Yes → TanStack Query
* No → サーバーコンポーネントの直接取得でOK
#### 実践的な構成例
'use client'
export function UserProfile() {
// 読み取りはTanStack Query(自動キャッシュ・同期)
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
// 書き込みはServer Actions(副作用のトリガー)
const mutation = useMutation({
mutationFn: updateUserName,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user'] })
});
return (
<button onClick={() => mutation.mutate('New Name')}>更新</button>
);
}
結論:道具の役割を履き違えるな
Server Actionsは「書く」ための道具であり、TanStack Queryは「守る」ための道具だ。
Server Actionsだけで完結させようとするのは、ハンマー一本で精密機器を修理しようとするようなものだ。確かに動くかもしれないが、その先には必ず「メンテナンス不能なスパゲッティコード」という結末が待っている。
エンジニアとして大切なのは、流行の技術を盲信することではない。その技術が「ユーザーのUX」と「コードの保守性」のどちらに寄与するのかを冷徹に見極めることだ。
キーボードを叩く手を止め、一度自分のコードが「再利用性の低い命令的な状態管理」に陥っていないか確認してほしい。その問いかけこそが、君を一段上のエンジニアへと引き上げるはずだ。