読み込み中...
Next.js 15のServer Actionsとzodを組み合わせることで、型安全かつ保守性の高いフォームバリデーションを実装できます。本記事では、基本的な実装からエラーハンドリング、ユーザー体験の向上まで、実践的な手法を段階的に解説します。
Server Actionsは、Next.js 13.4で導入され、Next.js 15で安定版となったサーバーサイドの関数実行機能です。クライアントコンポーネントから直接サーバー関数を呼び出せるため、APIルートを経由せずにデータ操作が可能になります。
主な特徴:
まず、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を使用することで、以下のメリットが得られます。
これらの実装パターンを組み合わせることで、プロダクショングレードのフォームバリデーションを構築できます。
コメント