معالجة النماذج والتحقق من الصحة
مقدمة إلى معالجة النماذج
النماذج ضرورية لتفاعل المستخدم في تطبيقات الويب. يوفر Next.js 13+ مع إجراءات الخادم أنماطاً قوية لمعالجة النماذج التي تكون بسيطة وآمنة في نفس الوقت. يغطي هذا الدرس التنفيذ الشامل للنماذج مع التحقق من الصحة، ومعالجة الأخطاء، وأفضل الممارسات.
نموذج أساسي مع إجراءات الخادم
لنبدأ بنموذج اتصال بسيط باستخدام إجراءات الخادم:
<!-- app/contact/page.tsx -->
import { submitContactForm } from './actions'
export default function ContactPage() {
return (
<div className="max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">اتصل بنا</h1>
<form action={submitContactForm} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-1">
الاسم
</label>
<input
type="text"
id="name"
name="name"
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
البريد الإلكتروني
</label>
<input
type="email"
id="email"
name="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-1">
الرسالة
</label>
<textarea
id="message"
name="message"
required
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
className="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700"
>
إرسال الرسالة
</button>
</form>
</div>
)
}
<!-- app/contact/actions.ts -->
'use server'
export async function submitContactForm(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
// معالجة بيانات النموذج
console.log({ name, email, message })
// في تطبيق حقيقي، ستحفظ في قاعدة البيانات أو ترسل بريداً إلكترونياً
// await prisma.contact.create({ data: { name, email, message } })
return { success: true }
}
React Hook Form للنماذج المعقدة
للنماذج المعقدة مع العديد من الحقول ومنطق التحقق، يوفر React Hook Form أداءً ممتازاً وتجربة مطور رائعة:
# تثبيت React Hook Form npm install react-hook-form
<!-- app/components/user-form.tsx -->
'use client'
import { useForm } from 'react-hook-form'
import { useState } from 'react'
interface UserFormData {
firstName: string
lastName: string
email: string
age: number
phone: string
address: string
city: string
zipCode: string
}
export function UserForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const {
register,
handleSubmit,
formState: { errors },
reset
} = useForm<UserFormData>()
const onSubmit = async (data: UserFormData) => {
setIsSubmitting(true)
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (response.ok) {
alert('تم إنشاء المستخدم بنجاح!')
reset()
}
} catch (error) {
console.error('خطأ في الإرسال:', error)
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
الاسم الأول
</label>
<input
{...register('firstName', {
required: 'الاسم الأول مطلوب',
minLength: {
value: 2,
message: 'يجب أن يكون الاسم الأول حرفين على الأقل'
}
})}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.firstName && (
<p className="text-red-500 text-sm mt-1">
{errors.firstName.message}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">
اسم العائلة
</label>
<input
{...register('lastName', {
required: 'اسم العائلة مطلوب'
})}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.lastName && (
<p className="text-red-500 text-sm mt-1">
{errors.lastName.message}
</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">البريد الإلكتروني</label>
<input
type="email"
{...register('email', {
required: 'البريد الإلكتروني مطلوب',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'عنوان بريد إلكتروني غير صالح'
}
})}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">العمر</label>
<input
type="number"
{...register('age', {
required: 'العمر مطلوب',
min: { value: 18, message: 'يجب أن يكون عمرك 18 عاماً على الأقل' },
max: { value: 120, message: 'عمر غير صالح' }
})}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.age && (
<p className="text-red-500 text-sm mt-1">{errors.age.message}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'جاري الإرسال...' : 'إرسال'}
</button>
</form>
)
}
Zod للتحقق الآمن من النوع
توفر Zod فحص نوع وقت التشغيل والتحقق من الصحة الذي يعمل بسلاسة مع TypeScript:
# تثبيت Zod npm install zod
<!-- lib/validations/user-schema.ts -->
import { z } from 'zod'
export const userSchema = z.object({
firstName: z.string()
.min(2, 'يجب أن يكون الاسم الأول حرفين على الأقل')
.max(50, 'يجب أن يكون الاسم الأول أقل من 50 حرفاً'),
lastName: z.string()
.min(2, 'يجب أن يكون اسم العائلة حرفين على الأقل')
.max(50, 'يجب أن يكون اسم العائلة أقل من 50 حرفاً'),
email: z.string()
.email('عنوان بريد إلكتروني غير صالح')
.toLowerCase(),
age: z.number()
.int('يجب أن يكون العمر رقماً صحيحاً')
.min(18, 'يجب أن يكون عمرك 18 عاماً على الأقل')
.max(120, 'عمر غير صالح'),
phone: z.string()
.regex(/^\+?[1-9]\d{1,14}$/, 'رقم هاتف غير صالح'),
password: z.string()
.min(8, 'يجب أن تكون كلمة المرور 8 أحرف على الأقل')
.regex(/[A-Z]/, 'يجب أن تحتوي كلمة المرور على حرف كبير واحد على الأقل')
.regex(/[a-z]/, 'يجب أن تحتوي كلمة المرور على حرف صغير واحد على الأقل')
.regex(/[0-9]/, 'يجب أن تحتوي كلمة المرور على رقم واحد على الأقل')
.regex(/[^A-Za-z0-9]/, 'يجب أن تحتوي كلمة المرور على حرف خاص واحد على الأقل'),
confirmPassword: z.string(),
website: z.string().url('عنوان URL غير صالح').optional().or(z.literal('')),
bio: z.string()
.max(500, 'يجب أن تكون السيرة الذاتية أقل من 500 حرف')
.optional(),
terms: z.boolean()
.refine(val => val === true, 'يجب عليك قبول الشروط والأحكام')
}).refine(data => data.password === data.confirmPassword, {
message: 'كلمات المرور غير متطابقة',
path: ['confirmPassword']
})
export type UserFormData = z.infer<typeof userSchema>
دمج React Hook Form مع Zod
ادمج React Hook Form مع Zod لأفضل تجربة مطور:
# تثبيت المحلل npm install @hookform/resolvers
<!-- app/components/validated-form.tsx -->
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { userSchema, type UserFormData } from '@/lib/validations/user-schema'
export function ValidatedForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 18,
phone: '',
password: '',
confirmPassword: '',
website: '',
bio: '',
terms: false
}
})
const onSubmit = async (data: UserFormData) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (response.ok) {
alert('تم التسجيل بنجاح!')
reset()
} else {
const error = await response.json()
alert(error.message || 'فشل التسجيل')
}
} catch (error) {
console.error('خطأ في الإرسال:', error)
alert('حدث خطأ. يرجى المحاولة مرة أخرى.')
}
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-md mx-auto p-6">
<h2 className="text-2xl font-bold mb-6">نموذج التسجيل</h2>
<div>
<label className="block text-sm font-medium mb-1">الاسم الأول</label>
<input
{...register('firstName')}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.firstName && (
<p className="text-red-500 text-sm mt-1">{errors.firstName.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">اسم العائلة</label>
<input
{...register('lastName')}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.lastName && (
<p className="text-red-500 text-sm mt-1">{errors.lastName.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">البريد الإلكتروني</label>
<input
type="email"
{...register('email')}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">العمر</label>
<input
type="number"
{...register('age', { valueAsNumber: true })}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.age && (
<p className="text-red-500 text-sm mt-1">{errors.age.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">كلمة المرور</label>
<input
type="password"
{...register('password')}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">تأكيد كلمة المرور</label>
<input
type="password"
{...register('confirmPassword')}
className="w-full px-3 py-2 border rounded-lg"
/>
{errors.confirmPassword && (
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword.message}</p>
)}
</div>
<div>
<label className="flex items-center gap-2">
<input
type="checkbox"
{...register('terms')}
className="w-4 h-4"
/>
<span className="text-sm">أوافق على الشروط والأحكام</span>
</label>
{errors.terms && (
<p className="text-red-500 text-sm mt-1">{errors.terms.message}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'جاري الإرسال...' : 'تسجيل'}
</button>
</form>
)
}
التحقق من جانب الخادم
تحقق دائماً من الخادم، حتى لو تحققت من العميل:
<!-- app/api/users/route.ts -->
import { NextResponse } from 'next/server'
import { userSchema } from '@/lib/validations/user-schema'
import { prisma } from '@/lib/prisma'
import bcrypt from 'bcryptjs'
export async function POST(request: Request) {
try {
const body = await request.json()
// التحقق من الصحة باستخدام Zod
const validatedData = userSchema.parse(body)
// التحقق من وجود البريد الإلكتروني
const existingUser = await prisma.user.findUnique({
where: { email: validatedData.email }
})
if (existingUser) {
return NextResponse.json(
{ error: 'البريد الإلكتروني مسجل بالفعل' },
{ status: 400 }
)
}
// تشفير كلمة المرور
const hashedPassword = await bcrypt.hash(validatedData.password, 12)
// إنشاء المستخدم
const user = await prisma.user.create({
data: {
firstName: validatedData.firstName,
lastName: validatedData.lastName,
email: validatedData.email,
age: validatedData.age,
phone: validatedData.phone,
hashedPassword,
bio: validatedData.bio,
website: validatedData.website,
}
})
return NextResponse.json(
{ message: 'تم إنشاء المستخدم بنجاح', userId: user.id },
{ status: 201 }
)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'فشل التحقق من الصحة', issues: error.issues },
{ status: 400 }
)
}
console.error('خطأ في التسجيل:', error)
return NextResponse.json(
{ error: 'خطأ داخلي في الخادم' },
{ status: 500 }
)
}
}
إجراءات الخادم مع التحقق من الصحة
استخدم إجراءات الخادم مع Zod لمعالجة نماذج آمنة من حيث النوع:
<!-- app/actions/user-actions.ts -->
'use server'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import bcrypt from 'bcryptjs'
const createUserSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
})
export async function createUser(prevState: any, formData: FormData) {
try {
const rawData = {
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
email: formData.get('email'),
password: formData.get('password'),
}
const validatedData = createUserSchema.parse(rawData)
const existingUser = await prisma.user.findUnique({
where: { email: validatedData.email }
})
if (existingUser) {
return {
success: false,
error: 'البريد الإلكتروني مسجل بالفعل'
}
}
const hashedPassword = await bcrypt.hash(validatedData.password, 12)
await prisma.user.create({
data: {
...validatedData,
hashedPassword,
}
})
revalidatePath('/users')
return {
success: true,
message: 'تم إنشاء المستخدم بنجاح'
}
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'فشل التحقق من الصحة',
issues: error.issues
}
}
return {
success: false,
error: 'فشل إنشاء المستخدم'
}
}
}
<!-- app/components/user-form-with-action.tsx -->
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { createUser } from '../actions/user-actions'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
className="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{pending ? 'جاري الإنشاء...' : 'إنشاء مستخدم'}
</button>
)
}
export function UserFormWithAction() {
const [state, formAction] = useFormState(createUser, null)
return (
<form action={formAction} className="space-y-4 max-w-md mx-auto p-6">
{state?.error && (
<div className="p-3 text-red-500 bg-red-50 rounded-lg">
{state.error}
</div>
)}
{state?.success && (
<div className="p-3 text-green-500 bg-green-50 rounded-lg">
{state.message}
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">الاسم الأول</label>
<input
type="text"
name="firstName"
required
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">اسم العائلة</label>
<input
type="text"
name="lastName"
required
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">البريد الإلكتروني</label>
<input
type="email"
name="email"
required
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">كلمة المرور</label>
<input
type="password"
name="password"
required
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<SubmitButton />
</form>
)
}
- أنشئ نموذج تسجيل متعدد الخطوات باستخدام React Hook Form
- قم بتنفيذ التحقق من مخطط Zod مع رسائل خطأ مخصصة
- أضف التحقق غير المتزامن للتحقق مما إذا كان البريد الإلكتروني متاحاً
- أنشئ إجراء خادم يتحقق ويحفظ بيانات النموذج
- قم بتنفيذ تحديثات واجهة المستخدم المتفائلة باستخدام useOptimistic
- أضف تحميل الملفات مع التحقق من حجم الصورة ونوعها
أفضل ممارسات النماذج
- تحقق دائماً من العميل والخادم
- قدم رسائل خطأ واضحة ومفيدة
- اعرض أخطاء التحقق مباشرة بالقرب من الحقول ذات الصلة
- عطل أزرار الإرسال أثناء الإرسال
- استخدم التحسين التقدمي مع إجراءات الخادم
- قم بتنفيذ حالات التحميل والتعليقات المناسبة
- امسح الحقول الحساسة (كلمات المرور) عند حدوث خطأ في الإرسال
- استخدم أنواع الإدخال المناسبة (email، tel، url) لتحسين تجربة المستخدم
الخلاصة
معالجة النماذج الحديثة في Next.js تجمع بين إجراءات الخادم، React Hook Form، والتحقق من Zod لتنفيذات نماذج آمنة من حيث النوع وعالية الأداء وآمنة. تحقق دائماً من العميل والخادم، وقدم تعليقات واضحة، واستخدم التحسين التقدمي لأفضل تجربة مستخدم.