⚡ Server Actions 完全ガイド:従来のAPIに別れを告げる
もしあなたが従来のReact(Create React App)やVueから移行してきた開発者なら、次の開発パターンに慣れているはずです:
「フロントエンドにButtonを配置し、クリック時にfetchをトリガーしてバックエンドAPI(例:/api/users/update)を呼び出し、バックエンド処理後にJSONをフロントエンドに返す」
この流れを私たちは10年以上続けてきましたが、Next.js App Routerが核兵器級の新機能Server Actionsを導入しました。 これによりフルスタック開発のロジックが完全に変革され、API Routeを一切書かずに、フロントエンドのボタンから直接バックエンド関数を「呼び出せる」ようになりました���
1. Server Actionsとは?
Server Actionsを使用すると、Server ComponentsやClient Components内で直接、サーバーサイドで実行される非同期関数を定義・呼び出せます。
従来のAPI手法の課題点:
- 2つのファイルを管理する必要がある:フロントエンドコンポーネントファイル(
page.tsx)とバックエンドAPIファイル(route.ts)。 - 型安全性が失われやすい:フロントエンドがJSONを送信し、バックエンドが受信する過程でTypeScriptの型がずれやすい。
- 状態管理が複雑:
isLoading、isError、dataなどのReact Stateを自前で管理する必要がある。
Server Actionsの利点:
- 1つのファイルで完結:フロントエンドとバックエンドのロジックを同じファイル(場合によっては同じコンポーネント内)に記述可能。
- 完璧な型安全性:フロントエンドからバックエンド関数を呼び出す際、TypeScriptがパラメータ型を完璧に推論。
- HTMLフォームと完全互換:JavaScriptの読み込み失敗や無効化時でもフォーム送信が可能!
2. 最初のServer Action:Hello World
最もシンプルな例から見ていきましょう。これはServer Componentで、データベースを更新するアクションを記述します。
// src/app/profile/page.tsx
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db'; // データベースインスタンスと仮定
export default async function ProfilePage() {
// ⭐️ これがServer Action!
// 'use server'を追加するだけで、Next.jsはこの関数がサーバーでのみ実行されると認識!
async function updateName(formData: FormData) {
'use server'; // 魔法の宣言
// フォームから入力値を取得
const newName = formData.get('name') as string;
// 直接データベースに書き込み!fetchもAPIパスも不要!
await db.user.update({ name: newName });
// Next.jsにキャッシュクリアと画面更新を指示
revalidatePath('/profile');
}
return (
<div className="p-8">
<h1 className="text-2xl">名前を変更</h1>
{/* Server Actionをフォームのaction属性に直接バインド */}
<form action={updateName} className="mt-4 flex gap-4">
<input
type="text"
name="name"
className="border p-2 rounded"
placeholder="新しい名前を入力"
required
/>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
保存
</button>
</form>
</div>
);
}
このコードの衝撃的な点は?**「APIルートが存在しない」**ことです!
ユーザーが「保存」を押すと、Next.jsは内部で特別なPOSTリクエストをサーバーに自動送信し、サーバーがupdateNameを実行後、更新されたHTMLを即座に返します。このプロセスが驚くほどスムーズです。
3. Client ComponentでのServer Actions活用
上記の例はServer Componentですが、useStateを使いたい場合やクールなクリックエフェクト(Client Component必須)が必要な場合、Server Actionsは使えるのでしょうか?
答えは:可能ですが、Server Actionを別ファイルに分離する必要があります。
手順1:独立したActionsファイルを作成
actions.tsファイルを作成し、ファイルの最上部に'use server'を追加。これでこのファイル内の全関数がServer Actionsとして扱われます。
// src/app/actions/userActions.ts
'use server'; // ファイル全体がサーバー専用と宣言
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
// 今回はFormDataではなく通常パラメータを受け取る
export async function updateUsername(userId: string, newName: string) {
try {
// ここで権限検証可能
// if (!isLoggedIn) throw new Error('未ログイン');
await db.user.update({
where: { id: userId },
data: { name: newName }
});
revalidatePath('/profile');
return { success: true, message: '更新成功!' };
} catch (error: any) {
return { success: false, message: error.message };
}
}
手順2:Client Componentで呼び出し
// src/app/profile/ClientProfile.tsx
'use client'; // Client Component宣言
import { useState, useTransition } from 'react';
import { updateUsername } from '../actions/userActions'; // バックエンド関数を直接import!
export default function ClientProfile({ userId, initialName }: { userId: string, initialName: string }) {
const [name, setName] = useState(initialName);
const [message, setMessage] = useState('');
// useTransitionはReact 18の新機能で非同期状態管理に使用
const [isPending, startTransition] = useTransition();
const handleSave = () => {
startTransition(async () => {
// 普通の関数のようにServer Actionを呼び出し!
// TypeScriptがパラメータ型を完璧にチェック!
const result = await updateUsername(userId, name);
if (result.success) {
setMessage(result.message);
} else {
setMessage(`エラー:${result.message}`);
}
});
};
return (
<div className="flex flex-col gap-4 max-w-sm">
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="border p-2"
/>
<button
onClick={handleSave}
disabled={isPending}
className="bg-indigo-500 text-white p-2 rounded disabled:bg-gray-400"
>
{isPending ? '保存中...' : '保存'}
</button>
{message && <p className="text-sm text-green-600">{message}</p>}
</div>
);
}
4. 高度なフォーム処理:useFormStateとuseFormStatus
もしJavaScript非対応環境もサポートするため、ネイティブの<form>タグにこだわる場合、Reactは2つの強力なHookを提供しています。
useFormStatus:フォーム送信状態の取得
isLoading変数を自作する必要はありません。ボタンを小さなコンポーネントに分離しましょう:
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
// このHookは外側の<form>の状態を自動取得!
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="bg-blue-600 text-white px-4 py-2"
>
{pending ? '処理中 (Loading...)' : '送信確認'}
</button>
);
}
useFormState:���ォームからのエラーメッセージ取得
'use client';
import { useFormState } from 'react-dom';
import { createPost } from './actions';
import { SubmitButton } from './SubmitButton';
const initialState = {
message: '',
errors: null,
};
export default function PostForm() {
// 第1引数がServer Action、第2引数が初期状態
const [state, formAction] = useFormState(createPost, initialState);
return (
// action属性がformActionに変更されている点に注意
<form action={formAction} className="flex flex-col gap-4">
<input type="text" name="title" required />
<textarea name="content" required />
{/* サーバーから返されたエラーメッセージを表示 */}
{state?.message && <p className="text-red-500">{state.message}</p>}
<SubmitButton />
</form>
);
}
5. セキュリティ警告:Server Actionsを甘く見るな!
Server Actionsの記法は通常の関数に似ていますが、本質的には公開APIと同じです。 ハッカーはあなたのフロントエンドUIを完全にバイパスし、直接パラメータをServer Actionに送り込むことが可能です!
❌ 致命的な間違いの例:
'use server';
export async function deletePost(postId: string) {
// 大惨事!他人のpostIdを知っているハッカーは誰の記事でも削除可能!
await db.post.delete({ where: { id: postId }});
}
✅ 堅牢な正しい実装例:
'use server';
import { getSession } from '@/lib/auth'; // 認証ロジック
export async function deletePost(postId: string) {
// 1. 常に関数の最初でログイン状態を確認!
const session = await getSession();
if (!session?.user) throw new Error('認証エラー');
// 2. 常にデータの所有者かどうかを確認!
const post = await db.post.findUnique({ where: { id: postId }});
if (post.authorId !== session.user.id) {
throw new Error('権限不足:自分の記事のみ削除可能');
}
// 3. 全てのセキュリティチェック通過後のみ処理実行
await db.post.delete({ where: { id: postId }});
}
Server Actionsを活用しましょう!適切な権限管理を実践すれば、開発速度はこれまでにないレベルに到達します!🚀