Next.js
قراءة في 3 دقائقنماذج آمنة الأنواع في Next.js: Server Actions وZod وuseActionState
مخطط Zod واحد مشترك بين العميل والخادم، وServer Action واحد، وuseActionState — بلا مكتبة نماذج ولا API route ولا اختلاف بين تحقق العميل والخادم.

هذا النمط بعينه — Server Action مع مخطط Zod وuseActionState — هو ما يشغّل كل نموذج في لوحة الإدارة في هذا الموقع. وهو يستحق أن يُكتب عنه بتأنٍّ، لأنه يحل محل قدر مفاجئ مما كان يحتاج إلى مكتبة نماذج وAPI route ومكتبة تحقق على العميل، تُوصَل كلها يدويًا.
الشكل العام
- يحدد مخطط Zod شكل البيانات الصالحة — مرة واحدة فقط، ويُشارَك بين رسائل الخطأ الظاهرة للعميل والتحقق على الخادم.
- يقرأ Server Action الـ
FormDataويتحقق منه بالمخطط، ثم يعيد أخطاء الحقول أو ينفّذ التعديل بنفسه. - يربط
useActionStateالنموذج بذلك الإجراء دون أيfetchأو JSON يدوي، ويعمل حتى بدون JavaScript (تحسين تدريجي) لأنه<form action>حقيقي.
1. المخطط
import { z } from "zod";
export const projectSchema = z.object({
slug: z.string().min(2).regex(/^[a-z0-9-]+$/, "Slug must be kebab-case"),
titleEn: z.string().min(1, "English title is required"),
order: z.coerce.number().int().default(0), // FormData values are always strings — coerce them
});2. الـ Server Action
"use server";
export type ActionState = {
success: boolean;
message?: string;
fieldErrors?: Record<string, string[]>;
};
export async function createProject(
_prev: ActionState,
formData: FormData
): Promise<ActionState> {
const parsed = projectSchema.safeParse({
slug: formData.get("slug"),
titleEn: formData.get("titleEn"),
order: formData.get("order") ?? 0,
});
if (!parsed.success) {
return {
success: false,
message: "Please fix the highlighted fields.",
fieldErrors: parsed.error.flatten().fieldErrors,
};
}
await projectRepository.create(parsed.data);
return { success: true };
}3. ربطه بالنموذج
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
const INITIAL_STATE: ActionState = { success: false };
function SubmitButton() {
const { pending } = useFormStatus(); // reads the pending state of the nearest parent <form>
return <button disabled={pending}>{pending ? "Saving…" : "Create"}</button>;
}
export default function ProjectForm() {
const [state, formAction] = useActionState(createProject, INITIAL_STATE);
return (
<form action={formAction}>
<input name="titleEn" />
{state.fieldErrors?.titleEn && <p>{state.fieldErrors.titleEn[0]}</p>}
<SubmitButton />
</form>
);
}ملاحظة مهمة: إعادة التوجيه
من المغري استدعاء redirect() في نهاية Server Action يعمل عبر useActionState — لكن هذا غير موثوق حاليًا في Next.js (قد يفشل أول إرسال في إعادة التوجيه بصمت). النمط الأوثق: أعِد { success: true } من الإجراء، ونفّذ إعادة التوجيه على العميل من useEffect يراقب تلك الحالة.
useEffect(() => {
if (state.success) {
router.push("/admin/projects");
router.refresh(); // re-fetches the destination page’s server data
}
}, [state.success, router]);لماذا هذا أفضل هنا من مكتبة نماذج على العميل
- مخطط واحد لا اثنان — لا إعداد منفصل للتحقق على العميل قد ينحرف عن قواعد الخادم.
- يعمل قبل اكتمال الـ hydration، ويتراجع إلى طلب POST حقيقي للنموذج بدون JavaScript.
- لا API route ولا
fetchولا تسلسل JSON يدوي ولا أنابيب لحالة التحميل غيرuseFormStatus.
هذا ليس بديلًا لشيء مثل React Hook Form في النماذج المعقدة جدًا والكثيرة التفاعل (المعالجات متعددة الخطوات ذات الاعتماد الكبير على العميل) — لكن في الحالة الشائعة جدًا «أنشئ هذا السجل أو عدّله»، فهو شيفرة أقل وأجزاء متحركة أقل ومصدر وحيد للحقيقة في التحقق.