日本語

Next.js 14のサーバーアクションに関する包括的なガイド。フォームハンドリングのベストプラクティス、データバリデーション、セキュリティの考慮事項、そしてモダンなWebアプリケーションを構築するための高度なテクニックを解説します。

Next.js 14 サーバーアクション:フォームハンドリングのベストプラクティスをマスターする

Next.js 14は、パフォーマンスが高くユーザーフレンドリーなWebアプリケーションを構築するための強力な機能を導入しています。その中でもサーバーアクションは、フォーム送信やデータミューテーションをサーバー上で直接処理する革新的な方法として際立っています。このガイドでは、Next.js 14のサーバーアクションに関する包括的な概要を提供し、フォームハンドリング、データバリデーション、セキュリティ、および高度なテクニックのベストプラクティスに焦点を当てます。実践的な例を探求し、堅牢でスケーラブルなWebアプリケーションを構築するのに役立つ実用的な知見を提供します。

Next.js サーバーアクションとは?

サーバーアクションは、サーバー上で実行され、Reactコンポーネントから直接呼び出すことができる非同期関数です。これにより、フォーム送信やデータミューテーションを処理するための従来のAPIルートが不要になり、コードの簡素化、セキュリティの向上、パフォーマンスの強化が実現します。サーバーアクションはReactサーバーコンポーネント(RSC)であり、サーバー上で実行されるため、初回ページロードが高速化され、SEOも向上します。

サーバーアクションの主な利点:

Next.js 14プロジェクトのセットアップ

サーバーアクションに飛び込む前に、Next.js 14プロジェクトがセットアップされていることを確認してください。ゼロから始める場合は、次のコマンドを使用して新しいプロジェクトを作成します:

npx create-next-app@latest my-next-app

サーバーコンポーネントとアクションを最大限に活用するために、プロジェクトがappディレクトリ構造を使用していることを確認してください。

サーバーアクションによる基本的なフォームハンドリング

簡単な例から始めましょう:データベースに新しいアイテムを作成するためにデータを送信するフォームです。入力フィールドと送信ボタンを持つシンプルなフォームを使用します。

例:新しいアイテムの作成

まず、Reactコンポーネント内にサーバーアクション関数を定義します。この関数は、サーバー上でフォーム送信ロジックを処理します。

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  // データベースとのやり取りをシミュレート
  console.log('アイテムを作成中:', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // 遅延をシミュレート

  console.log('アイテムが正常に作成されました!');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    await createItem(formData);
    setIsSubmitting(false);
  }

  return (
    
); }

解説:

データバリデーション

データバリデーションは、データの整合性を確保し、セキュリティの脆弱性を防ぐために不可欠です。サーバーアクションは、サーバーサイドでバリデーションを実行する絶好の機会を提供します。このアプローチは、クライアントサイドのバリデーションのみに関連するリスクを軽減するのに役立ちます。

例:入力データのバリデーション

createItem サーバーアクションを変更して、バリデーションロジックを含めます。

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  if (!name || name.length < 3) {
    throw new Error('アイテム名は3文字以上である必要があります。');
  }

  // データベースとのやり取りをシミュレート
  console.log('アイテムを作成中:', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // 遅延をシミュレート

  console.log('アイテムが正常に作成されました!');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'エラーが発生しました。');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

解説:

バリデーションライブラリの使用

より複雑なバリデーションシナリオでは、次のようなバリデーションライブラリの使用を検討してください:

以下にZodを使用した例を示します:

// app/utils/validation.ts
import { z } from 'zod';

export const CreateItemSchema = z.object({
  name: z.string().min(3, 'アイテム名は3文字以上である必要があります。'),
});
// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';
import { CreateItemSchema } from '../utils/validation';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  const validatedFields = CreateItemSchema.safeParse({ name });

  if (!validatedFields.success) {
    return { errors: validatedFields.error.flatten().fieldErrors };
  }

  // データベースとのやり取りをシミュレート
  console.log('アイテムを作成中:', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // 遅延をシミュレート

  console.log('アイテムが正常に作成されました!');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'エラーが発生しました。');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

解説:

セキュリティに関する考慮事項

サーバーアクションはサーバー上でコードを実行することでセキュリティを強化しますが、一般的な脅威からアプリケーションを保護するためには、セキュリティのベストプラクティスに従うことが依然として重要です。

クロスサイトリクエストフォージェリ(CSRF)の防止

CSRF攻撃は、ウェブサイトがユーザーのブラウザに対して持つ信頼を悪用します。CSRF攻撃を防ぐためには、CSRF保護メカニズムを実装してください。

Next.jsは、サーバーアクションを使用する際にCSRF保護を自動的に処理します。フレームワークは各フォーム送信に対してCSRFトークンを生成および検証し、リクエストがアプリケーションから発信されたものであることを保証します。

ユーザー認証と認可の処理

認可されたユーザーのみが特定のアクションを実行できるようにしてください。機密データや機能を保護するために、認証および認可メカニズムを実装します。

以下にNextAuth.jsを使用してサーバーアクションを保護する例を示します:

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';
import { getServerSession } from 'next-auth';
import { authOptions } from '../../app/api/auth/[...nextauth]/route';

async function createItem(formData: FormData) {
  'use server'

  const session = await getServerSession(authOptions);

  if (!session) {
    throw new Error('認証されていません');
  }

  const name = formData.get('name') as string;

  // データベースとのやり取りをシミュレート
  console.log('アイテムを作成中:', name, '、ユーザー:', session.user?.email);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // 遅延をシミュレート

  console.log('アイテムが正常に作成されました!');
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'エラーが発生しました。');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

解説:

入力データのサニタイズ

クロスサイトスクリプティング(XSS)攻撃を防ぐために入力データをサニタイズしてください。XSS攻撃は、悪意のあるコードがウェブサイトに注入され、ユーザーデータやアプリケーションの機能が損なわれる可能性がある場合に発生します。

サーバーアクションで処理する前に、DOMPurifysanitize-html のようなライブラリを使用して、ユーザーが提供した入力をサニタイズしてください。

高度なテクニック

基本をカバーしたので、サーバーアクションを効果的に使用するためのいくつかの高度なテクニックを探ってみましょう。

オプティミスティック(楽観的)アップデート

オプティミスティックアップデートは、サーバーが確認する前であっても、アクションが成功するかのように即座にUIを更新することで、より良いユーザーエクスペリエンスを提供します。サーバーでアクションが失敗した場合、UIは以前の状態に戻されます。

// app/components/UpdateItemForm.tsx
'use client';

import { useState } from 'react';

async function updateItem(id: string, formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  // データベースとのやり取りをシミュレート
  console.log('アイテムを更新中:', id, '、名前:', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // 遅延をシミュレート

  // 失敗をシミュレート(デモンストレーション目的)
  const shouldFail = Math.random() < 0.5;
  if (shouldFail) {
    throw new Error('アイテムの更新に失敗しました。');
  }

  console.log('アイテムが正常に更新されました!');
  return { name }; // 更新された名前を返す
}

export default function UpdateItemForm({ id, initialName }: { id: string; initialName: string }) {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  const [itemName, setItemName] = useState(initialName);

  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);

    // UIを楽観的に更新
    const newName = formData.get('name') as string;
    setItemName(newName);

    try {
      const result = await updateItem(id, formData);
      // 成功した場合、更新はすでにsetItemNameを介してUIに反映されている

    } catch (error: any) {
      setErrorMessage(error.message || 'エラーが発生しました。');
      // エラー時にUIを元に戻す
      setItemName(initialName);
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    

現在の名前: {itemName}

{errorMessage &&

{errorMessage}

}
); }

解説:

データの再検証

サーバーアクションがデータを変更した後、UIが最新の変更を反映するように、キャッシュされたデータを再検証する必要がある場合があります。Next.jsはデータを再検証するためのいくつかの方法を提供します:

以下に、新しいアイテムを作成した後にパスを再検証する例を示します:

// app/components/CreateItemForm.tsx
'use client';

import { useState } from 'react';
import { revalidatePath } from 'next/cache';

async function createItem(formData: FormData) {
  'use server'

  const name = formData.get('name') as string;

  // データベースとのやり取りをシミュレート
  console.log('アイテムを作成中:', name);

  await new Promise((resolve) => setTimeout(resolve, 1000)); // 遅延をシミュレート

  console.log('アイテムが正常に作成されました!');

  revalidatePath('/items'); // /items パスを再検証
}

export default function CreateItemForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true);
    setErrorMessage(null);
    try {
      await createItem(formData);
    } catch (error: any) {
      setErrorMessage(error.message || 'エラーが発生しました。');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    
{errorMessage &&

{errorMessage}

}
); }

解説:

サーバーアクションのベストプラクティス

サーバーアクションの利点を最大限に活用するために、以下のベストプラクティスを検討してください:

よくある落とし穴とその回避方法

サーバーアクションは多くの利点を提供しますが、注意すべきいくつかの一般的な落とし穴があります:

結論

Next.js 14のサーバーアクションは、フォーム送信とデータミューテーションをサーバー上で直接処理するための強力で効率的な方法を提供します。このガイドで概説されたベストプラクティスに従うことで、堅牢で安全、かつ高性能なWebアプリケーションを構築できます。サーバーアクションを活用して、コードを簡素化し、セキュリティを強化し、全体的なユーザーエクスペリエンスを向上させてください。これらの原則を統合する際には、開発の選択がもたらすグローバルな影響を考慮してください。フォームとデータ処理プロセスが、多様な国際的なオーディエンスにとってアクセシブルで、安全で、ユーザーフレンドリーであることを確認してください。この包括性へのコミットメントは、アプリケーションの使いやすさを向上させるだけでなく、そのリーチと効果をグローバルな規模で拡大させることにも繋がります。