•11 min read

Next.js14でのフォーム処理:Server Actions vs Client Components

Next.js14でのフォーム処理:Server Actions vs Client Components

Next.jsの急速に進化するエコシステムにおいて、フォームハンドリングは大きなパラダイムシフトを遂げました。Next.js 14の安定版リリースにより、冗長なクライアントサイドのフォームハンドラーを記述したり、過剰なローディング状態を管理したり、従来のAPIルートと格闘したりする時代は終わりました。Server Actionsは、Reactのモダンなフック(useFormStateとuseFormStatus)と組み合わせることで、堅牢なフォームを構築するための深く統合された、段階的に強化されたアプローチを提供します。

Zodのようなスキーマ検証ライブラリと組み合わせることで、開発者はクライアントとサーバーの境界を越えて厳格な型安全性を強制できるようになりました。この包括的なガイドでは、Next.js 14でフォームを処理する方法を探り、従来のクライアントコンポーネントとモダンなServer Actionsのアプローチを比較します。

Audio Briefing
0:00 / 0:00

古いパラダイム:クライアントサイドのフォームハンドリング

Next.js 14とApp Routerの成熟以前は、フォームハンドリングは主にクライアントサイドで行われていました。開発者はReactの状態(useState)や、React Hook FormやFormikのような専用ライブラリに大きく依存していました。典型的なフローは次のとおりです。

  1. デフォルトのフォーム送信イベントを防止する。
  2. フォームデータを収集し、解析する。
  3. APIルートに非同期のフェッチリクエストを送信する。
  4. isLoading、isError、およびisSuccessの状態を手動で管理する。
  5. APIレスポンスに基づいてUIを更新する。

このアプローチは機能しますが、大量のボイラープレートコードが発生し、より多くのJavaScriptをクライアントにプッシュすることになります。典型的なクライアントサイドのフォームは次のようになります。

'use client';

import { useState } from 'react';

export default function TraditionalForm() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    const formData = new FormData(e.currentTarget);
    const data = Object.fromEntries(formData);

    try {
      const res = await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(data),
      });

      if (!res.ok) throw new Error('Submission failed');
      // Handle success
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="title" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Submitting...' : 'Submit'}
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

このパターンは効果的ですが、冗長です。完全に独立したAPIルートが必要であり、コンポーネントに'use client'をマークすることを強制するため、クライアントバンドルサイズが増加します。

Advertisement

新しい標準:Next.js 14 Server Actions

Server Actionsは、非同期のサーバー関数をReactコンポーネント内(または別のファイル内)に直接定義し、それを<form>要素のaction属性から直接呼び出すことを可能にすることで、このパラダイムを覆します。

Server Actionsとは?

Server Actionsの核心は、サーバー上で実行される非同期関数です。これらはNext.jsのキャッシュおよび再検証システムとシームレスに統合されており、手動で状態の無効化ロジックを記述することなく、データを変更してUIを即座に更新できます。

Server Actionsを使用して、前の例をリファクタリングする方法を見てみましょう。

import { revalidatePath } from 'next/cache';

// This function runs exclusively on the server
async function submitData(formData) {
  'use server';
  
  const title = formData.get('title');
  const content = formData.get('content');

  // Perform database mutation here
  // await db.posts.create({ title, content });

  // Revalidate the cache to show the new post
  revalidatePath('/posts');
}

export default function ServerActionForm() {
  return (
    <form action={submitData}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      <button type="submit">Submit</button>
    </form>
  );
}

'use client'、useState、およびfetchがないことに注目してください。このフォームは、ユーザーがブラウザでJavaScriptを無効にしている場合でも機能し、すぐに使えるプログレッシブエンハンスメントを提供します。しかし、ローディング状態や検証エラーについてはどうでしょうか?そこでReactの新しいフックとZodが活躍します。

useFormStateとuseFormStatusによる状態管理

リッチなユーザーエクスペリエンスを作成するには、フォーム送信中にフィードバックを提供する必要があります。Reactは、Server Actionsを使用するフォームのために特別に設計された2つの新しいフックを導入しました。

useFormStatus

useFormStatusは、最後のフォーム送信のステータス情報を提供します。これは、<form>の内部でレンダリングされるコンポーネントで使用する必要があります。

'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending} className="bg-blue-600 text-white px-4 py-2 rounded">
      {pending ? 'Submitting...' : 'Save Post'}
    </button>
  );
}

useFormState

useFormStateを使用すると、Server Actionの結果に基づいて状態を更新できます。これは、検証エラーや成功メッセージを処理するのに最適です。useFormStateとZodを組み合わせて、堅牢なスキーマ駆動型検証を行いましょう。

Zodによる堅牢な検証

Zodは、TypeScriptファーストのスキーマ宣言および検証ライブラリです。Server Actionsと組み合わせると、データベースに到達するデータが期待どおりであることを保証する上で非常に強力です。

まず、スキーマとフォームの状態の形状を定義します。

import { z } from 'zod';

const postSchema = z.object({
  title: z.string().min(3, 'Title must be at least 3 characters'),
  content: z.string().min(10, 'Content must be at least 10 characters'),
});

次に、Server Actionを作成します。これはZodのsafeParseを使用して、受信するFormDataを検証します。

'use server';

import { revalidatePath } from 'next/cache';
// Assume postSchema is imported here

export async function createPost(prevState, formData) {
  // Validate form fields using Zod
  const validatedFields = postSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  // If form validation fails, return errors early
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Missing Fields. Failed to Create Post.',
    };
  }

  // Mutate data
  try {
    // await db.posts.create(validatedFields.data);
    
    revalidatePath('/posts');
    return { message: 'Post created successfully!', errors: {} };
  } catch (error) {
    return { message: 'Database Error: Failed to Create Post.', errors: {} };
  }
}

最後に、これらすべてをクライアントコンポーネントで結合します。フォームのレンダリングと状態管理はクライアントで行われますが、実際の実行と検証はサーバー上で安全に維持されます。

'use client';

import { useFormState } from 'react-dom';
import { createPost } from '@/app/actions';
import { SubmitButton } from './SubmitButton';

export default function CreatePostForm() {
  const initialState = { message: null, errors: {} };
  const [state, dispatch] = useFormState(createPost, initialState);

  return (
    <form action={dispatch} className="flex flex-col gap-4 max-w-md">
      <div>
        <label htmlFor="title">Title</label>
        <input id="title" name="title" type="text" className="border p-2 w-full" />
        {state.errors?.title && (
          <p className="text-red-500 text-sm mt-1">{state.errors.title[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="content">Content</label>
        <textarea id="content" name="content" className="border p-2 w-full" />
        {state.errors?.content && (
          <p className="text-red-500 text-sm mt-1">{state.errors.content[0]}</p>
        )}
      </div>

      <SubmitButton />

      {state.message && (
        <p className="text-sm text-gray-700 bg-gray-100 p-3 rounded" aria-live="polite">
          {state.message}
        </p>
      )}
    </form>
  );
}
Advertisement

クライアントコンポーネント vs Server Actions:使い分け

Server ActionsはNext.jsのデータ変更の未来を象徴していますが、従来のクライアントコンポーネントも複雑なアプリケーションで依然としてその役割を果たします。

Server Actionsを使用する場合:

  • データを安全に変更する必要がある場合(データベース操作、隠された秘密を含むAPI呼び出し)。
  • プログレッシブエンハンスメントが必要な場合(JavaScriptが無効な状態でも機能するフォーム)。
  • クライアントサイドのバンドルサイズを削減したい場合。
  • キャッシュの再検証を密接に結合したい場合(revalidatePathまたはrevalidateTagを使用)。

クライアントサイドのハンドリングを使用する場合:

  • 状態を継続的にサーバーに送信することなく、ステップ間で状態を維持する必要がある複雑な多段階ウィザードが必要な場合。
  • 送信前にリアルタイムで、キー入力ごとの検証が必要な場合。
  • ブラウザ環境を前提とし、ローカル状態の統合を必要とする複雑なサードパーティのクライアントサイドライブラリに大きく依存している場合。

結論

Next.js 14は、フォームハンドリングに関する開発者のエクスペリエンスを劇的に簡素化しました。Server Actions、useFormState、useFormStatus、およびZodを採用することで、非常に堅牢で高性能、かつ型安全なフォームを構築できます。このアーキテクチャは、任意のローディング状態やエラー状態を手動で管理する認知的負荷を軽減するだけでなく、クライアントサイドのレンダリングとサーバーサイドの実行の間でよりクリーンな関心の分離を強制します。

Reactエコシステムが進化し続ける中で、これらのネイティブAPIを標準化することは、アプリケーションが堅牢でアクセスしやすく、ウェブの未来に対応できることを保証します。過去の重厚なアプローチから、これらのプログレッシブエンハンスメント対応の方法へと移行することは、現代のウェブ開発者にとって重要な一歩です。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement