[SAMPLE] Next.js 15 Server Actionsフォーム実践

2 months ago
5

Next.js 15のServer Actionsで実装するフォームバリデーション実践ガイド

Next.js 15のServer Actionsとzodを組み合わせることで、型安全かつ保守性の高いフォームバリデーションを実装できます。本記事では、基本的な実装からエラーハンドリング、ユーザー体験の向上まで、実践的な手法を段階的に解説します。

Server Actionsとは

Server Actionsは、Next.js 13.4で導入され、Next.js 15で安定版となったサーバーサイドの関数実行機能です。クライアントコンポーネントから直接サーバー関数を呼び出せるため、APIルートを経由せずにデータ操作が可能になります。

主な特徴:

  • フォーム送信時の自動的なプログレッシブエンハンスメント
  • JavaScriptが無効でも動作する段階的な機能向上
  • 型安全な関数呼び出し
  • 自動的なリクエスト重複排除

基本的な実装

まず、zodを使った型安全なバリデーションスキーマを定義します。

// app/actions/user.ts 'use server' import { z } from 'zod' const userSchema = z.object({ name: z.string().min(2, '名前は2文字以上で入力してください'), email: z.string().email('有効なメールアドレスを入力してください'), age: z.number().min(18, '18歳以上である必要があります').max(120, '有効な年齢を入力してください'), }) type UserFormState = { errors?: { name?: string[] email?: string[] age?: string[] } message?: string } export async function createUser( prevState: UserFormState, formData: FormData ): Promise<UserFormState> { const validatedFields = userSchema.safeParse({ name: formData.get('name'), email: formData.get('email'), age: Number(formData.get('age')), }) if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: '入力内容に誤りがあります。', } } try { // データベースへの保存処理 await saveUser(validatedFields.data) return { message: 'ユーザーを登録しました。', } } catch (error) { return { message: 'データベースエラーが発生しました。', } } }

クライアントコンポーネントでの利用

useFormStateフックとuseFormStatusフックを組み合わせて、リアルタイムなフィードバックを提供します。

// app/components/UserForm.tsx 'use client' import { useFormState, useFormStatus } from 'react-dom' import { createUser } from '@/app/actions/user' function SubmitButton() { const { pending } = useFormStatus() return ( <button type="submit" disabled={pending} className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50" > {pending ? '送信中...' : '登録する'} </button> ) } export function UserForm() { const [state, formAction] = useFormState(createUser, {}) return ( <form action={formAction} className="space-y-4"> <div> <label htmlFor="name" className="block text-sm font-medium"> 名前 </label> <input id="name" name="name" type="text" className="mt-1 block w-full rounded-md border-gray-300" aria-describedby="name-error" /> {state.errors?.name && ( <p id="name-error" className="mt-1 text-sm text-red-600"> {state.errors.name[0]} </p> )} </div> <div> <label htmlFor="email" className="block text-sm font-medium"> メールアドレス </label> <input id="email" name="email" type="email" className="mt-1 block w-full rounded-md border-gray-300" aria-describedby="email-error" /> {state.errors?.email && ( <p id="email-error" className="mt-1 text-sm text-red-600"> {state.errors.email[0]} </p> )} </div> <div> <label htmlFor="age" className="block text-sm font-medium"> 年齢 </label> <input id="age" name="age" type="number" className="mt-1 block w-full rounded-md border-gray-300" aria-describedby="age-error" /> {state.errors?.age && ( <p id="age-error" className="mt-1 text-sm text-red-600"> {state.errors.age[0]} </p> )} </div> {state.message && ( <p className={state.errors ? 'text-red-600' : 'text-green-600'}> {state.message} </p> )} <SubmitButton /> </form> ) }

応用: 楽観的更新とリアルタイムバリデーション

useOptimisticフックを使用することで、サーバーレスポンスを待たずにUIを更新し、より滑らかなユーザー体験を提供できます。

'use client' import { useOptimistic } from 'react' import { useFormState } from 'react-dom' import { createUser } from '@/app/actions/user' type User = { id: string name: string email: string } export function UserList({ initialUsers }: { initialUsers: User[] }) { const [optimisticUsers, addOptimisticUser] = useOptimistic( initialUsers, (state, newUser: User) => [...state, newUser] ) const [state, formAction] = useFormState(createUser, {}) async function handleSubmit(formData: FormData) { const name = formData.get('name') as string const email = formData.get('email') as string // 楽観的にUIを更新 addOptimisticUser({ id: crypto.randomUUID(), name, email, }) // サーバーアクションを実行 await formAction(formData) } return ( <div> <ul className="space-y-2"> {optimisticUsers.map((user) => ( <li key={user.id} className="p-4 border rounded"> <p className="font-medium">{user.name}</p> <p className="text-sm text-gray-600">{user.email}</p> </li> ))} </ul> {/* フォームは省略 */} </div> ) }

エラーハンドリングのベストプラクティス

1. フィールドレベルのエラー表示

各入力フィールドの直下にエラーメッセージを表示することで、ユーザーは何を修正すべきか即座に理解できます。

2. アクセシビリティの考慮

aria-describedby属性を使用してスクリーンリーダーのユーザーにもエラー情報を提供します。

3. 段階的な機能向上

JavaScriptが無効な環境でも、HTMLのネイティブバリデーションとサーバーサイドバリデーションで基本的な機能を提供します。

パフォーマンス最適化

Suspense境界の活用

フォーム送信中の状態を適切に処理するため、Suspense境界を設定します。

import { Suspense } from 'react' export default function Page() { return ( <Suspense fallback={<FormSkeleton />}> <UserForm /> </Suspense> ) }

リクエスト重複排除

Server Actionsは自動的にリクエストの重複を排除しますが、明示的に制御したい場合はuseTransitionを使用します。

'use client' import { useTransition } from 'react' export function UserForm() { const [isPending, startTransition] = useTransition() function handleSubmit(formData: FormData) { startTransition(async () => { await createUser(formData) }) } return ( <form action={handleSubmit}> {/* フォーム内容 */} {isPending && <p>送信中...</p>} </form> ) }

まとめ

Next.js 15のServer Actionsを使用することで、以下のメリットが得られます。

  • 型安全性: zodとTypeScriptによる厳密な型チェック
  • 保守性: バリデーションロジックの一元管理
  • ユーザー体験: 楽観的更新による滑らかなインタラクション
  • アクセシビリティ: 段階的な機能向上とスクリーンリーダー対応

これらの実装パターンを組み合わせることで、プロダクショングレードのフォームバリデーションを構築できます。

Comments

0
0
0