أتقن خطاف useFormState في React. دليل شامل لإدارة حالة النماذج بسلاسة، والتحقق من الصحة من جانب الخادم، وتحسين تجربة المستخدم مع إجراءات الخادم (Server Actions).
خطاف React useFormState: نظرة عميقة على إدارة النماذج والتحقق من صحتها في الويب الحديث
تُعد النماذج حجر الزاوية في التفاعل على الويب. من نماذج الاتصال البسيطة إلى المعالجات المعقدة متعددة الخطوات، فهي ضرورية لإدخال بيانات المستخدم وإرسالها. لسنوات، تنقل مطورو React عبر مجموعة من حلول إدارة الحالة، بدءًا من خطافات useState البسيطة للسيناريوهات الأساسية إلى المكتبات القوية من جهات خارجية مثل Formik و React Hook Form للاحتياجات الأكثر تعقيدًا. في حين أن هذه الأدوات ممتازة، فإن React تتطور باستمرار لتوفير أدوات أساسية أكثر تكاملاً وقوة.
نقدم لكم useFormState، وهو خطاف تم تقديمه في React 18. صُمم في البداية للعمل بسلاسة مع إجراءات خادم React (React Server Actions)، ويقدم useFormState نهجًا مبسطًا وقويًا ومدمجًا لإدارة حالة النماذج، خاصة عند التعامل مع منطق الخادم والتحقق من الصحة. إنه يبسط عملية عرض الملاحظات من الخادم، مثل أخطاء التحقق من الصحة أو رسائل النجاح، مباشرة داخل واجهة المستخدم الخاصة بك.
سيأخذك هذا الدليل الشامل في رحلة عميقة داخل خطاف useFormState. سنستكشف مفاهيمه الأساسية، والتطبيقات العملية، والأنماط المتقدمة، وكيف يتناسب مع النظام البيئي الأوسع لتطوير React الحديث. سواء كنت تبني تطبيقات باستخدام Next.js أو Remix أو React الخام، فإن فهم useFormState سيزودك بأداة قوية لبناء نماذج أفضل وأكثر مرونة.
ما هو `useFormState` ولماذا نحتاجه؟
في جوهره، useFormState هو خطاف مصمم لتحديث الحالة بناءً على نتيجة إجراء نموذج. فكر فيه كنسخة متخصصة من useReducer مصممة خصيصًا لعمليات إرسال النماذج. إنه يسد الفجوة بأناقة بين تفاعل المستخدم من جانب العميل والمعالجة من جانب الخادم.
قبل useFormState، كان تدفق إرسال النموذج النموذجي الذي يتضمن خادمًا قد يبدو كالتالي:
- يملأ المستخدم نموذجًا.
- تتتبع الحالة من جانب العميل (باستخدام
useStateعلى سبيل المثال) قيم الإدخال. - عند الإرسال، يمنع معالج الأحداث (
onSubmit) السلوك الافتراضي للمتصفح. - يتم إنشاء طلب
fetchيدويًا وإرساله إلى نقطة نهاية API للخادم. - تتم إدارة حالات التحميل (على سبيل المثال،
const [isLoading, setIsLoading] = useState(false)). - يعالج الخادم الطلب، ويقوم بالتحقق من الصحة، ويتفاعل مع قاعدة البيانات.
- يرسل الخادم استجابة JSON (على سبيل المثال،
{ success: false, errors: { email: 'Invalid format' } }). - يقوم الكود من جانب العميل بتحليل هذه الاستجابة وتحديث متغير حالة آخر لعرض الأخطاء أو رسائل النجاح.
هذه العملية، على الرغم من أنها وظيفية، تتضمن قدرًا كبيرًا من الكود المتكرر لإدارة حالات التحميل وحالات الخطأ ودورة الطلب/الاستجابة. يبسط useFormState، خاصة عند إقرانه بإجراءات الخادم، هذا الأمر بشكل كبير عن طريق إنشاء تدفق أكثر تكاملاً وتصريحية.
الفوائد الأساسية لاستخدام useFormState هي:
- تكامل سلس مع الخادم: إنه الحل المدمج للتعامل مع الاستجابات من إجراءات الخادم، مما يجعل التحقق من الصحة من جانب الخادم جزءًا أساسيًا ومدمجًا في مكونك.
- إدارة مبسطة للحالة: يركز منطق تحديثات حالة النموذج، مما يقلل من الحاجة إلى خطافات
useStateمتعددة للبيانات والأخطاء وحالة الإرسال. - التحسين التدريجي: يمكن للنماذج المبنية باستخدام
useFormStateوإجراءات الخادم أن تعمل حتى لو تم تعطيل JavaScript على العميل، حيث إنها مبنية على أساس إرسال نماذج HTML القياسية. - تجربة مستخدم محسنة: يسهل تقديم ملاحظات فورية وسياقية للمستخدم، مثل أخطاء التحقق من الصحة المضمنة أو رسائل النجاح، مباشرة بعد إرسال النموذج.
فهم توقيع خطاف `useFormState`
لإتقان الخطاف، دعنا أولاً نحلل توقيعه وقيمه المرجعة. الأمر أبسط مما قد يبدو عليه في البداية.
const [state, formAction] = useFormState(action, initialState);
المعلمات (Parameters):
action: هذه دالة سيتم تنفيذها عند إرسال النموذج. تتلقى هذه الدالة وسيطتين: الحالة السابقة للنموذج وبيانات النموذج المرسلة. ومن المتوقع أن تعيد الحالة الجديدة. عادةً ما يكون هذا إجراء خادم (Server Action)، ولكنه يمكن أن يكون أي دالة.initialState: هذه هي القيمة التي تريد أن تكون عليها حالة النموذج في البداية، قبل حدوث أي عملية إرسال. يمكن أن تكون أي قيمة قابلة للتسلسل (سلسلة نصية، رقم، كائن، إلخ).
القيم المرجعة (Return Values):
يعيد useFormState مصفوفة تحتوي على عنصرين بالضبط:
state: الحالة الحالية للنموذج. في أول تصيير، ستكون هذه هيinitialStateالتي قدمتها. بعد إرسال النموذج، ستكون القيمة التي أعادتها دالةactionالخاصة بك. هذه الحالة هي ما تستخدمه لتصيير ملاحظات واجهة المستخدم، مثل رسائل الخطأ.formAction: دالة إجراء جديدة تمررها إلى خاصيةactionفي عنصر<form>الخاص بك. عند تشغيل هذا الإجراء (عن طريق إرسال النموذج)، ستقوم React باستدعاء دالةactionالأصلية الخاصة بك مع الحالة السابقة وبيانات النموذج، ثم تقوم بتحديثstateبالنتيجة.
قد يبدو هذا النمط مألوفًا إذا كنت قد استخدمت useReducer. دالة action تشبه المخفض (reducer)، و initialState هي الحالة الأولية، وتتولى React عملية الإرسال (dispatching) نيابة عنك عند إرسال النموذج.
مثال عملي أول: نموذج اشتراك بسيط
دعنا ننشئ نموذج اشتراك بسيط في نشرة إخبارية لنرى useFormState قيد التنفيذ. سيكون لدينا حقل إدخال بريد إلكتروني واحد وزر إرسال. سيقوم إجراء الخادم بإجراء تحقق أساسي للتأكد من تقديم بريد إلكتروني وما إذا كان بتنسيق صالح.
أولاً، دعنا نحدد إجراء الخادم الخاص بنا. إذا كنت تستخدم Next.js، يمكنك وضع هذا في نفس ملف المكون الخاص بك عن طريق إضافة التوجيه 'use server'; في أعلى الدالة.
// In actions.js or at the top of your component file with 'use server'
export async function subscribe(previousState, formData) {
const email = formData.get('email');
if (!email) {
return { message: 'Email is required.' };
}
// A simple regex for demonstration purposes
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
return { message: 'Please enter a valid email address.' };
}
// Here you would typically save the email to a database
console.log(`Subscribing with email: ${email}`);
// Simulate a delay
await new Promise(res => setTimeout(res, 1000));
return { message: 'Thank you for subscribing!' };
}
الآن، دعنا ننشئ مكون العميل الذي يستخدم هذا الإجراء مع useFormState.
'use client';
import { useFormState } from 'react-dom';
import { subscribe } from './actions';
const initialState = {
message: null,
};
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
<h3>Subscribe to Our Newsletter</h3>
<div>
<label htmlFor="email">Email Address</label>
<input type="email" id="email" name="email" required />
</div>
<button type="submit">Subscribe</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
دعنا نحلل ما يحدث:
- نستورد
useFormStateمنreact-dom(ملاحظة: ليس منreact). - نحدد كائن
initialState. هذا يضمن أن متغيرstateالخاص بنا له شكل ثابت منذ التصيير الأول. - نستدعي
useFormState(subscribe, initialState). هذا يربط حالة مكوننا بإجراء الخادمsubscribe. - يتم تمرير
formActionالمرجع إلى خاصيةactionلعنصر<form>. هذا هو الاتصال السحري. - نقوم بتصيير الرسالة من كائن
stateالخاص بنا بشكل شرطي. في التصيير الأول، يكونstate.messageهوnull، لذلك لا يظهر شيء. - عندما يرسل المستخدم النموذج، تستدعي React
formAction. هذا يشغل إجراء الخادمsubscribeالخاص بنا. تتلقى دالةsubscribepreviousState(في البداية،initialStateالخاص بنا) وformData. - يقوم إجراء الخادم بتشغيل منطقه ويعيد كائن حالة جديد (على سبيل المثال،
{ message: 'Email is required.' }). - تتلقى React هذه الحالة الجديدة وتعيد تصيير مكون
SubscriptionForm. يحتوي متغيرstateالآن على الكائن الجديد، وتعرض الفقرة الشرطية لدينا رسالة الخطأ أو النجاح.
هذا قوي بشكل لا يصدق. لقد قمنا بتنفيذ حلقة تحقق كاملة بين العميل والخادم بأقل قدر من الكود المتكرر لإدارة الحالة من جانب العميل.
تحسين تجربة المستخدم مع `useFormStatus`
يعمل نموذجنا، ولكن يمكن أن تكون تجربة المستخدم أفضل. عندما ينقر المستخدم على "Subscribe"، يظل الزر نشطًا، ولا يوجد مؤشر مرئي على أن شيئًا ما يحدث حتى يستجيب الخادم. هذا هو المكان الذي يأتي فيه خطاف useFormStatus.
يوفر خطاف useFormStatus معلومات الحالة حول آخر عملية إرسال للنموذج. بشكل حاسم، يجب استخدامه في مكون يكون ابنًا لعنصر <form>. لا يعمل إذا تم استدعاؤه في نفس المكون الذي يصيّر النموذج.
دعنا ننشئ مكون SubmitButton منفصل.
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Subscribing...' : 'Subscribe'}
</button>
);
}
الآن، يمكننا تحديث SubscriptionForm لاستخدام هذا المكون الجديد:
// ... imports
import { SubmitButton } from './SubmitButton';
// ... initialState and other code
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
{/* ... form inputs ... */}
<SubmitButton /> {/* Replace the old button */}
{state?.message && <p>{state.message}</p>}
</form>
);
}
مع هذا التغيير، عند إرسال النموذج، تصبح قيمة pending من useFormStatus هي true. يعيد مكون SubmitButton الخاص بنا التصيير، مع تعطيل الزر وتغيير نصه إلى "Subscribing...". بمجرد اكتمال إجراء الخادم وتحديث useFormState للحالة، لم يعد النموذج معلقًا، ويعود الزر إلى حالته الأصلية. يوفر هذا ملاحظات أساسية للمستخدم ويمنع عمليات الإرسال المزدوجة.
التحقق المتقدم مع حالات الأخطاء المنظمة و Zod
سلسلة رسالة واحدة جيدة للنماذج البسيطة، ولكن التطبيقات الواقعية غالبًا ما تتطلب أخطاء تحقق لكل حقل. يمكننا تحقيق ذلك بسهولة عن طريق إعادة كائن حالة أكثر تنظيمًا من إجراء الخادم الخاص بنا.
دعنا نعزز إجراءنا لإعادة كائن بمفتاح errors، والذي يحتوي بدوره على رسائل لحقول معينة. هذه أيضًا فرصة مثالية لتقديم مكتبة للتحقق من صحة المخططات مثل Zod لمنطق تحقق أكثر قوة وقابلية للصيانة.
الخطوة 1: تثبيت Zod
npm install zod
الخطوة 2: تحديث إجراء الخادم
سنقوم بإنشاء مخطط Zod لتحديد الشكل المتوقع وقواعد التحقق من صحة بيانات النموذج لدينا. بعد ذلك، سنستخدم schema.safeParse() للتحقق من formData الواردة.
'use server';
import { z } from 'zod';
// Define the schema for our form
const contactSchema = z.object({
name: z.string().min(2, { message: 'Name must be at least 2 characters.' }),
email: z.string().email({ message: 'Invalid email address.' }),
message: z.string().min(10, { message: 'Message must be at least 10 characters.' }),
});
export async function submitContactForm(previousState, formData) {
const validatedFields = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// If validation fails, return the errors
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Validation failed. Please check your inputs.',
};
}
// If validation succeeds, process the data
// For example, send an email or save to a database
console.log('Success!', validatedFields.data);
// ... processing logic ...
// Return a success state
return {
errors: {},
message: 'Thank you for your message! We will get back to you soon.',
};
}
لاحظ كيف نستخدم validatedFields.error.flatten().fieldErrors. هذه أداة مساعدة يدوية من Zod تحول كائن الخطأ إلى بنية أكثر قابلية للاستخدام، مثل: { name: ['Name must be at least 2 characters.'], message: ['Message is too short'] }.
الخطوة 3: تحديث مكون العميل
الآن، سنقوم بتحديث مكون النموذج الخاص بنا للتعامل مع حالة الخطأ المنظمة هذه.
'use client';
import { useFormState } from 'react-dom';
import { submitContactForm } from './actions';
import { SubmitButton } from './SubmitButton'; // Assuming we have a submit button
const initialState = {
message: null,
errors: {},
};
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, initialState);
return (
<form action={formAction}>
<h2>Contact Us</h2>
<div>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" />
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
{state.errors?.message && (
<p className="error">{state.errors.message[0]}</p>
)}
</div>
<SubmitButton />
{state.message && <p className="form-status">{state.message}</p>}
</form>
);
}
هذا النمط قابل للتطوير وقوي بشكل لا يصدق. يصبح إجراء الخادم الخاص بك هو مصدر الحقيقة الوحيد لمنطق التحقق من الصحة، ويوفر Zod طريقة تصريحية وآمنة للأنواع لتعريف تلك القواعد. يصبح مكون العميل ببساطة مستهلكًا للحالة التي يوفرها useFormState، ويعرض الأخطاء حيث تنتمي. هذا الفصل بين الاهتمامات يجعل الكود أنظف وأسهل في الاختبار وأكثر أمانًا، حيث يتم فرض التحقق من الصحة دائمًا على الخادم.
`useFormState` مقابل حلول إدارة النماذج الأخرى
مع كل أداة جديدة يأتي السؤال: "متى يجب أن أستخدم هذا بدلاً مما أعرفه بالفعل؟" دعنا نقارن useFormState بالنهج الشائعة الأخرى.
`useFormState` مقابل `useState`
- `useState` مثالي للنماذج البسيطة الخاصة بالعميل فقط أو عندما تحتاج إلى إجراء تفاعلات معقدة في الوقت الفعلي من جانب العميل (مثل التحقق المباشر أثناء كتابة المستخدم) قبل الإرسال. يمنحك تحكمًا مباشرًا ودقيقًا.
- `useFormState` يتفوق عندما يتم تحديد حالة النموذج بشكل أساسي من خلال استجابة الخادم. إنه مصمم لدورة الطلب/الاستجابة لإرسال النموذج وهو الخيار الأمثل عند استخدام إجراءات الخادم. إنه يزيل الحاجة إلى إدارة استدعاءات fetch وحالات التحميل وتحليل الاستجابة يدويًا.
`useFormState` مقابل المكتبات الخارجية (React Hook Form, Formik)
مكتبات مثل React Hook Form و Formik هي حلول ناضجة وغنية بالميزات تقدم مجموعة شاملة من الأدوات لإدارة النماذج. توفر:
- التحقق المتقدم من الصحة من جانب العميل (غالبًا مع تكامل المخططات لـ Zod، Yup، إلخ).
- إدارة حالة معقدة للحقول المتداخلة ومصفوفات الحقول والمزيد.
- تحسينات الأداء (على سبيل المثال، عزل إعادة التصيير للمدخلات التي تتغير فقط).
- مساعدات للمكونات المتحكم بها والتكامل مع مكتبات واجهة المستخدم.
إذًا، متى تختار أيهما؟
- اختر
useFormStateعندما:- تستخدم إجراءات خادم React وتريد حلاً مدمجًا ومتكاملاً.
- مصدر الحقيقة الأساسي للتحقق من الصحة هو الخادم.
- تقدر التحسين التدريجي وتريد أن تعمل نماذجك بدون JavaScript.
- منطق النموذج الخاص بك بسيط نسبيًا ويتمحور حول دورة الإرسال/الاستجابة.
- اختر مكتبة خارجية عندما:
- تحتاج إلى تحقق شامل ومعقد من جانب العميل مع ملاحظات فورية (على سبيل المثال، التحقق عند فقدان التركيز أو عند التغيير).
- لديك نماذج ديناميكية للغاية (على سبيل المثال، إضافة/إزالة الحقول، منطق شرطي).
- لا تستخدم إطار عمل مع إجراءات الخادم وتقوم ببناء طبقة اتصال خاصة بك بين العميل والخادم باستخدام واجهات برمجة تطبيقات REST أو GraphQL.
- تحتاج إلى تحكم دقيق في الأداء وإعادة التصيير في النماذج الكبيرة جدًا.
من المهم أيضًا ملاحظة أن هذه الخيارات ليست حصرية. يمكنك استخدام React Hook Form لإدارة الحالة من جانب العميل والتحقق من صحة النموذج الخاص بك، ثم استخدام معالج الإرسال الخاص به لاستدعاء إجراء خادم. ومع ذلك، بالنسبة للعديد من حالات الاستخدام الشائعة، يوفر الجمع بين useFormState وإجراءات الخادم حلاً أبسط وأكثر أناقة.
أفضل الممارسات والمزالق الشائعة
للحصول على أقصى استفادة من useFormState، ضع في اعتبارك أفضل الممارسات التالية:
- حافظ على تركيز الإجراءات: يجب أن تكون دالة إجراء النموذج مسؤولة عن شيء واحد: معالجة إرسال النموذج. وهذا يشمل التحقق من الصحة، وتعديل البيانات (الحفظ في قاعدة بيانات)، وإعادة الحالة الجديدة. تجنب الآثار الجانبية غير المتعلقة بنتيجة النموذج.
- حدد شكل حالة ثابت: ابدأ دائمًا بـ
initialStateمحدد جيدًا وتأكد من أن الإجراء الخاص بك يعيد دائمًا كائنًا بنفس الشكل، حتى عند النجاح. هذا يمنع أخطاء وقت التشغيل على العميل عند محاولة الوصول إلى خصائص مثلstate.errors. - اعتنق التحسين التدريجي: تذكر أن إجراءات الخادم تعمل بدون JavaScript من جانب العميل. صمم واجهة المستخدم الخاصة بك للتعامل مع كلا السيناريوهين برشاقة. على سبيل المثال، تأكد من أن رسائل التحقق المصيرة من الخادم واضحة، حيث لن يستفيد المستخدم من حالة الزر المعطل بدون JS.
- افصل اهتمامات واجهة المستخدم: استخدم مكونات مثل
SubmitButtonلتغليف واجهة المستخدم المعتمدة على الحالة. هذا يحافظ على مكون النموذج الرئيسي أنظف ويحترم قاعدة أنuseFormStatusيجب استخدامه في مكون ابن. - لا تنسَ إمكانية الوصول: عند عرض الأخطاء، استخدم سمات ARIA مثل
aria-invalidعلى حقول الإدخال الخاصة بك وربط رسائل الخطأ بمدخلاتها المعنية باستخدامaria-describedbyلضمان إمكانية وصول المستخدمين الذين يستخدمون قارئات الشاشة إلى نماذجك.
مأزق شائع: استخدام useFormStatus في نفس المكون
خطأ متكرر هو استدعاء useFormStatus في نفس المكون الذي يصيّر وسم <form>. لن يعمل هذا لأن الخطاف يحتاج إلى أن يكون داخل سياق النموذج للوصول إلى حالته. قم دائمًا باستخراج جزء واجهة المستخدم الذي يحتاج إلى الحالة (مثل الزر) إلى مكون ابن خاص به.
الخاتمة
يمثل خطاف useFormState، بالاقتران مع إجراءات الخادم، تطورًا كبيرًا في كيفية تعاملنا مع النماذج في React. إنه يدفع المطورين نحو نموذج تحقق أكثر قوة وتركيزًا على الخادم مع تبسيط إدارة الحالة من جانب العميل. من خلال تجريد تعقيدات دورة حياة الإرسال، فإنه يسمح لنا بالتركيز على ما هو أكثر أهمية: تحديد منطق أعمالنا وبناء تجربة مستخدم سلسة.
في حين أنه قد لا يحل محل المكتبات الخارجية الشاملة لكل حالة استخدام، يوفر useFormState أساسًا قويًا ومدمجًا ومحسنًا تدريجيًا للغالبية العظمى من النماذج في تطبيقات الويب الحديثة. من خلال إتقان أنماطه وفهم مكانه في نظام React البيئي، يمكنك بناء نماذج أكثر مرونة وقابلية للصيانة وسهولة في الاستخدام باستخدام كود أقل ووضوح أكبر.