Next.js App Routerの深淵:Server ActionsとReact Queryの「危うい共存」を制する
Next.js 14/15のApp Routerを触っていて、深夜にふと手を止めたことはないか。「Server Actionsだけで完結させるべきか、それともReact Query(TanStack Query)を導入すべきか」。この境界線を見誤ると、UIはガタつき、キャッシュは不整合を起こし、最終的には「なぜ動いているのか誰にも分からない」コードの墓場が出来上がる。
今日は、この「魔の領域」で泥沼にハマり、何度もキーボードを投げ出しそうになった末に見つけた、実戦的な解法を共有する。
1. なぜ「Server Actionsだけ」では破綻するのか
「API Routeを書かなくていい」「型安全」という甘い言葉に誘われて、Server ActionsだけでUIを構築しようとすると、必ず以下の壁にぶち当たる。
汚物化したコードの断片
例えば、フォーム送信に伴う「ロード中のスピナー表示」「エラーハンドリング」「成功後の即時UI更新」をServer Actionsだけで書こうとすると、コンポーネント内は管理不能な`useState`の海になる。// 悪夢の例:Server Actions単体での状態管理
export default function TodoForm() {
const [loading, setLoading] = useState(false); // 状態1
const [error, setError] = useState(null); // 状態2
const [data, setData] = useState(null); // 状態3
const handleSubmit = async (formData: FormData) => {
setLoading(true); // 手動での状態管理が散乱する
try {
await addTodo(formData);
// 画面更新のために再取得が必要だが、ここからどう制御する?
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
};
// ...以下、UIの条件分岐地獄
}
このアプローチは、小規模なフォームならまだしも、インタラクティブなアプリでは「ローディング状態」や「楽観的アップデート」の管理が手動となり、コードの保守性が壊滅する。
2. ベストプラクティス:役割の厳格な分離
たどり着いた結論はシンプルだ。「Server Actionsはバックエンドのインターフェース」「React Queryはフロントエンドの状態管理」と役割を割り切る。
役割分担の設計図
| 機能 | 採用ツール | 理由 |
|---|---|---|
| 初期データ取得 | RSC | SEOと初期表示速度の最大化 |
| データ更新 | Server Actions | DB操作の簡潔さと型安全性 |
| 状態管理・UI更新 | React Query | 楽観的アップデート・キャッシュ制御 |
3. 実践:Server ActionsをReact Queryで包む
Server Actionsを`mutationFn`としてReact Queryに渡すことで、Server Actionsの「手軽さ」とReact Queryの「UX制御力」を両立できる。
// hooks/useTodos.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { addTodo } from '@/actions/todo';
export function useAddTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addTodo, // Server Actionをそのまま渡す
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 楽観的アップデート:レスポンスを待たずにUIを書き換える
queryClient.setQueryData(['todos'], (old: any) => [...old, newTodo]);
},
onSettled: () => {
// 最終的なデータ整合性を確保
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
}
【重要】キャッシュの二重管理を回避せよ
ここで最も注意すべきは、Server Actions内で`revalidatePath`を呼ぶべきではないという点だ。もしServer Actionで`revalidatePath`を呼び、同時にReact Queryで`invalidateQueries`を呼ぶと、App Routerのサーバーキャッシュとクライアント側のキャッシュが競合し、UIが一瞬チラつく「二重再レンダリング」が発生する。
トレードオフの受け入れ:
`revalidatePath`を捨て、React Query側にキャッシュ制御を一任するということは、「サーバー主導のキャッシュ更新」を一部放棄することを意味する。これは、JSが無効な環境や、極めて強固なキャッシュ戦略が必要な場合にはリスクとなる。しかし、現代的なSPAライクな体験をNext.jsで実現する上では、この「フロントへの権限委譲」が最も現実的な選択肢だ。
4. 型安全を極める
`useActionState`(旧`useFormState`)とReact Queryを併用する場合、型定義を疎かにしてはいけない。戻り値の型を明示することで、開発体験は劇的に向上する。
// actions/todo.tsの型定義
export type ActionResponse = {
success: boolean;
message?: string;
data?: Todo;
};
export async function addTodo(prevState: any, formData: FormData): Promise<ActionResponse> {
// ...処理
return { success: true, data: newTodo };
}
この型を`useMutation`のジェネリクスに渡すことで、コンポーネント側で「成功した時のデータ」と「エラーハンドリング」が完全に型補完される。
結びに:コードに「生きてる実感」を
Server Actionsは強力な「武器」だ。しかし、それを直接UIに直結させると、UXという繊細な防具を破壊してしまう。
React Queryという「防具」を重ねて装備することで、初めてApp Routerの真の力が解放される。深夜のデバッグでモニターに八つ当たりしたくなった時、この記事を思い出してほしい。泥臭い試行錯誤の先には、必ずクリーンで高速なコードが待っている。
さあ、コーヒーを淹れ直して、続きを書こうぜ。