⚡ 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手法の課題点:

  1. 2つのファイルを管理する必要がある:フロントエンドコンポーネントファイル(page.tsx)とバックエンドAPIファイル(route.ts)。
  2. 型安全性が失われやすい:フロントエンドがJSONを送信し、バックエンドが受信する過程でTypeScriptの型がずれやすい。
  3. 状態管理が複雑isLoadingisErrordataなどのReact Stateを自前で管理する必要がある。

Server Actionsの利点:

  1. 1つのファイルで完結:フロントエンドとバックエンドのロジックを同じファイル(場合によっては同じコンポーネント内)に記述可能。
  2. 完璧な型安全性:フロントエンドからバックエンド関数を呼び出す際、TypeScriptがパラメータ型を完璧に推論。
  3. 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. 高度なフォーム処理:useFormStateuseFormStatus

もし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を活用しましょう!適切な権限管理を実践すれば、開発速度はこれまでにないレベルに到達します!🚀

完全なチュートリアルをロック解除

このチャプターは有料コンテンツです。プロジェクトに参加して、10以上の神レベルのPromptや実際のソースコード例を含む、5000字以上の深い分析をロック解除してください!