أطلق العنان لقوة خطاف useActionState في React. تعلم كيف يبسط إدارة النماذج، ويتعامل مع حالات الانتظار، ويعزز تجربة المستخدم بأمثلة عملية ومتعمقة.
React useActionState: دليل شامل لإدارة النماذج الحديثة
عالم تطوير الويب في تطور مستمر، ونظام React البيئي في طليعة هذا التغيير. مع الإصدارات الأخيرة، قدمت React ميزات قوية تحسن بشكل أساسي كيفية بناء تطبيقات تفاعلية ومرنة. من بين أكثر هذه الميزات تأثيرًا هو خطاف useActionState، الذي يغير قواعد اللعبة في التعامل مع النماذج والعمليات غير المتزامنة. هذا الخطاف، الذي كان يُعرف سابقًا باسم useFormState في الإصدارات التجريبية، أصبح الآن أداة مستقرة وأساسية لأي مطور React حديث.
سيأخذك هذا الدليل الشامل في رحلة متعمقة لاستكشاف useActionState. سنتناول المشكلات التي يحلها، وآلياته الأساسية، وكيفية الاستفادة منه جنبًا إلى جنب مع خطافات تكميلية مثل useFormStatus لإنشاء تجارب مستخدم فائقة. سواء كنت تبني نموذج اتصال بسيطًا أو تطبيقًا معقدًا كثيف البيانات، فإن فهم useActionState سيجعل الكود الخاص بك أكثر نظافة، وأكثر وضوحًا، وأكثر قوة.
المشكلة: تعقيد إدارة حالة النماذج التقليدية
قبل أن نتمكن من تقدير أناقة useActionState، يجب أولاً أن نفهم التحديات التي يعالجها. لسنوات، كانت إدارة حالة النماذج في React تتضمن نمطًا متوقعًا ولكنه غالبًا ما يكون مرهقًا باستخدام خطاف useState.
لنفكر في سيناريو شائع: نموذج بسيط لإضافة منتج جديد إلى قائمة. نحتاج إلى إدارة عدة أجزاء من الحالة:
- قيمة الإدخال لاسم المنتج.
- حالة تحميل أو انتظار لإعطاء المستخدم ملاحظات أثناء استدعاء API.
- حالة خطأ لعرض الرسائل إذا فشل الإرسال.
- حالة نجاح أو رسالة عند الاكتمال.
قد يبدو التنفيذ النموذجي شيئًا كهذا:
مثال: 'الطريقة القديمة' باستخدام خطافات useState المتعددة
// دالة API وهمية
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('يجب أن يتكون اسم المنتج من 3 أحرف على الأقل.');
}
console.log(`تمت إضافة المنتج "${productName}".`);
return { success: true };
};
// المكون
{error}import { useState } from 'react';
function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);
try {
await addProductAPI(productName);
setProductName(''); // مسح حقل الإدخال عند النجاح
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};
return (
id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>
{isPending ? 'جاري الإضافة...' : 'إضافة منتج'}
{error &&
);
}
هذا النهج يعمل، لكن له عدة عيوب:
- الكود المكرر (Boilerplate): نحتاج إلى ثلاث استدعاءات منفصلة لـ useState لإدارة ما هو من الناحية المفاهيمية عملية إرسال نموذج واحدة.
- إدارة الحالة اليدوية: المطور مسؤول عن تعيين وإعادة تعيين حالات التحميل والخطأ يدويًا بالترتيب الصحيح داخل كتلة try...catch...finally. هذا الأمر متكرر وعرضة للأخطاء.
- الاقتران (Coupling): منطق التعامل مع نتيجة إرسال النموذج مقترن بشدة بمنطق عرض المكون.
تقديم useActionState: نقلة نوعية
useActionState هو خطاف React مصمم خصيصًا لإدارة حالة إجراء غير متزامن، مثل إرسال نموذج. إنه يبسط العملية بأكملها عن طريق ربط الحالة مباشرة بنتيجة دالة الإجراء.
توقيعه واضح وموجز:
const [state, formAction] = useActionState(actionFn, initialState);
لنفصل مكوناته:
actionFn(previousState, formData)
: هذه هي دالتك غير المتزامنة التي تؤدي العمل (على سبيل المثال، استدعاء API). تتلقى الحالة السابقة وبيانات النموذج كوسائط. الأهم من ذلك، أن ما تعيده هذه الدالة يصبح الحالة الجديدة.initialState
: هذه هي قيمة الحالة قبل تنفيذ الإجراء لأول مرة.state
: هذه هي الحالة الحالية. تحتفظ بـ initialState في البداية ويتم تحديثها إلى القيمة المُرجعة من actionFn بعد كل تنفيذ.formAction
: هذه نسخة جديدة ومغلفة من دالة الإجراء الخاصة بك. يجب عليك تمرير هذه الدالة إلى خاصيةaction
لعنصر<form>
. تستخدم React هذه الدالة المغلفة لتتبع حالة الانتظار للإجراء.
مثال عملي: إعادة الهيكلة باستخدام useActionState
الآن، دعنا نعيد هيكلة نموذج المنتج الخاص بنا باستخدام useActionState. التحسين واضح على الفور.
أولاً، نحتاج إلى تكييف منطق الإجراء الخاص بنا. بدلاً من رمي الأخطاء، يجب أن يعيد الإجراء كائن حالة يصف النتيجة.
مثال: 'الطريقة الجديدة' باستخدام useActionState
// دالة الإجراء، مصممة للعمل مع useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // محاكاة تأخير الشبكة
if (!productName || productName.length < 3) {
return { message: 'يجب أن يتكون اسم المنتج من 3 أحرف على الأقل.', success: false };
}
console.log(`تمت إضافة المنتج "${productName}".`);
// عند النجاح، أعد رسالة نجاح وامسح النموذج.
return { message: `تمت إضافة "${productName}" بنجاح`, success: true };
};
// المكون المعاد هيكلته
{state.message} {state.message}import { useActionState } from 'react';
// ملاحظة: سنضيف useFormStatus في القسم التالي للتعامل مع حالة الانتظار.
function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
انظر كم هو أنظف الآن! لقد استبدلنا ثلاثة خطافات useState بخطاف useActionState واحد. أصبحت مسؤولية المكون الآن هي عرض واجهة المستخدم بناءً على كائن `state` فقط. كل منطق العمل مغلف بأناقة داخل دالة `addProductAction`. يتم تحديث الحالة تلقائيًا بناءً على ما يعيده الإجراء.
لكن انتظر، ماذا عن حالة الانتظار؟ كيف نعطل الزر أثناء إرسال النموذج؟
التعامل مع حالات الانتظار باستخدام useFormStatus
توفر React خطافًا مصاحبًا، وهو useFormStatus، مصمم لحل هذه المشكلة بالضبط. يوفر معلومات الحالة لآخر عملية إرسال للنموذج، ولكن بقاعدة حاسمة: يجب استدعاؤه من مكون يتم عرضه داخل <form>
الذي تريد تتبع حالته.
هذا يشجع على فصل الاهتمامات بشكل نظيف. تقوم بإنشاء مكون مخصص لعناصر واجهة المستخدم التي تحتاج إلى أن تكون على دراية بحالة إرسال النموذج، مثل زر الإرسال.
يعيد خطاف useFormStatus كائنًا به عدة خصائص، وأهمها `pending`.
const { pending, data, method, action } = useFormStatus();
pending
: قيمة منطقية تكون `true` إذا كان النموذج الأصل قيد الإرسال حاليًا و`false` بخلاف ذلك.data
: كائن `FormData` يحتوي على البيانات التي يتم إرسالها.method
: سلسلة نصية تشير إلى طريقة HTTP (`'get'` أو `'post'`).action
: إشارة إلى الدالة التي تم تمريرها إلى خاصية `action` للنموذج.
إنشاء زر إرسال مدرك للحالة
دعنا ننشئ مكون `SubmitButton` مخصصًا وندمجه في نموذجنا.
مثال: مكون SubmitButton
import { useFormStatus } from 'react-dom';
// ملاحظة: يتم استيراد useFormStatus من 'react-dom'، وليس 'react'.
function SubmitButton() {
const { pending } = useFormStatus();
return (
{pending ? 'جاري الإضافة...' : 'إضافة منتج'}
);
}
الآن، يمكننا تحديث مكون النموذج الرئيسي الخاص بنا لاستخدامه.
مثال: النموذج الكامل مع useActionState و useFormStatus
{state.message} {state.message}import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (دالة addProductAction تبقى كما هي)
function SubmitButton() { /* ... كما هو محدد أعلاه ... */ }
function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);
return (
{/* يمكننا إضافة مفتاح لإعادة تعيين حقل الإدخال عند النجاح */}
{!state.success && state.message && (
)}
{state.success && state.message && (
)}
);
}
بهذه البنية، لا يحتاج مكون `CompleteProductForm` إلى معرفة أي شيء عن حالة الانتظار. فمكون `SubmitButton` مستقل تمامًا. هذا النمط التركيبي قوي بشكل لا يصدق لبناء واجهات مستخدم معقدة وقابلة للصيانة.
قوة التحسين التدريجي (Progressive Enhancement)
واحدة من أعمق فوائد هذا النهج الجديد القائم على الإجراءات، خاصة عند استخدامه مع إجراءات الخادم (Server Actions)، هي التحسين التدريجي التلقائي. هذا مفهوم حيوي لبناء تطبيقات لجمهور عالمي، حيث يمكن أن تكون ظروف الشبكة غير موثوقة وقد يكون لدى المستخدمين أجهزة قديمة أو JavaScript معطلة.
إليك كيف يعمل:
- بدون JavaScript: إذا لم يقم متصفح المستخدم بتنفيذ JavaScript من جانب العميل، فإن
<form action={...}>
يعمل كنموذج HTML قياسي. يقوم بطلب صفحة كاملة إلى الخادم. إذا كنت تستخدم إطار عمل مثل Next.js، يتم تشغيل الإجراء من جانب الخادم، ويعيد إطار العمل عرض الصفحة بأكملها بالحالة الجديدة (على سبيل المثال، عرض خطأ التحقق من الصحة). يكون التطبيق يعمل بكامل وظائفه، فقط بدون سلاسة تطبيقات الصفحة الواحدة (SPA). - مع JavaScript: بمجرد تحميل حزمة JavaScript وترطيب (hydrate) React للصفحة، يتم تنفيذ نفس `formAction` من جانب العميل. بدلاً من إعادة تحميل الصفحة بالكامل، يتصرف مثل طلب fetch نموذجي. يتم استدعاء الإجراء، وتحديث الحالة، وإعادة عرض الأجزاء الضرورية فقط من المكون.
هذا يعني أنك تكتب منطق النموذج الخاص بك مرة واحدة، ويعمل بسلاسة في كلا السيناريوهين. أنت تبني تطبيقًا مرنًا ومتاحًا للجميع افتراضيًا، وهو فوز كبير لتجربة المستخدم في جميع أنحاء العالم.
أنماط متقدمة وحالات استخدام
1. إجراءات الخادم مقابل إجراءات العميل
يمكن أن تكون `actionFn` التي تمررها إلى useActionState دالة غير متزامنة قياسية من جانب العميل (كما في أمثلتنا) أو إجراء خادم (Server Action). إجراء الخادم هو دالة محددة على الخادم يمكن استدعاؤها مباشرة من مكونات العميل. في أطر العمل مثل Next.js، يمكنك تحديد واحدة بإضافة توجيه "use server";
في أعلى جسم الدالة.
- إجراءات العميل (Client Actions): مثالية للتغييرات التي تؤثر فقط على حالة جانب العميل أو تستدعي واجهات برمجة تطبيقات (APIs) تابعة لجهات خارجية مباشرة من العميل.
- إجراءات الخادم (Server Actions): مثالية للتغييرات التي تتضمن قاعدة بيانات أو موارد أخرى من جانب الخادم. إنها تبسط بنيتك عن طريق إزالة الحاجة إلى إنشاء نقاط نهاية API يدويًا لكل تغيير.
الجمال هو أن useActionState يعمل بشكل متطابق مع كليهما. يمكنك استبدال إجراء عميل بإجراء خادم دون تغيير كود المكون.
2. التحديثات المتفائلة (Optimistic Updates) مع `useOptimistic`
للحصول على إحساس أكثر استجابة، يمكنك دمج useActionState مع خطاف useOptimistic. التحديث المتفائل هو عندما تقوم بتحديث واجهة المستخدم على الفور، *بافتراض* أن الإجراء غير المتزامن سينجح. إذا فشل، فإنك تعيد واجهة المستخدم إلى حالتها السابقة.
تخيل تطبيق وسائط اجتماعية حيث تضيف تعليقًا. بشكل متفائل، ستعرض التعليق الجديد في القائمة على الفور بينما يتم إرسال الطلب إلى الخادم. تم تصميم useOptimistic للعمل جنبًا إلى جنب مع الإجراءات لجعل هذا النمط سهل التنفيذ.
3. إعادة تعيين النموذج عند النجاح
أحد المتطلبات الشائعة هو مسح حقول إدخال النموذج بعد الإرسال الناجح. هناك عدة طرق لتحقيق ذلك باستخدام useActionState.
- خدعة خاصية `key`: كما هو موضح في مثال `CompleteProductForm`، يمكنك تعيين `key` فريد لحقل إدخال أو للنموذج بأكمله. عندما يتغير المفتاح، ستقوم React بإلغاء تحميل المكون القديم وتحميل مكون جديد، مما يعيد تعيين حالته بشكل فعال. ربط المفتاح بعلامة نجاح (`key={state.success ? 'success' : 'initial'}`) هي طريقة بسيطة وفعالة.
- المكونات المتحكم بها (Controlled Components): لا يزال بإمكانك استخدام المكونات المتحكم بها إذا لزم الأمر. عن طريق إدارة قيمة حقل الإدخال باستخدام useState، يمكنك استدعاء دالة الضبط لمسحها داخل useEffect الذي يستمع لحالة النجاح من useActionState.
الأخطاء الشائعة وأفضل الممارسات
- موضع
useFormStatus
: تذكر، يجب عرض المكون الذي يستدعي useFormStatus كابن لـ<form>
. لن يعمل إذا كان شقيقًا أو أبًا له. - الحالة القابلة للتسلسل (Serializable State): عند استخدام إجراءات الخادم، يجب أن يكون كائن الحالة المُرجع من الإجراء الخاص بك قابلاً للتسلسل. هذا يعني أنه لا يمكن أن يحتوي على دوال، أو رموز (Symbols)، أو قيم أخرى غير قابلة للتسلسل. التزم بالكائنات البسيطة، والمصفوفات، والسلاسل النصية، والأرقام، والقيم المنطقية.
- لا ترمي أخطاء (Throw) في الإجراءات: بدلاً من `throw new Error()`، يجب أن تتعامل دالة الإجراء الخاصة بك مع الأخطاء بأمان وتعيد كائن حالة يصف الخطأ (على سبيل المثال، `{ success: false, message: 'حدث خطأ' }`). هذا يضمن تحديث الحالة دائمًا بشكل متوقع.
- حدد شكل حالة واضح: قم بإنشاء بنية متسقة لكائن الحالة الخاص بك من البداية. شكل مثل `{ data: T | null, message: string | null, success: boolean, errors: Record
| null }` يمكن أن يغطي العديد من حالات الاستخدام.
useActionState مقابل useReducer: مقارنة سريعة
للوهلة الأولى، قد يبدو useActionState مشابهًا لـ useReducer، حيث يتضمن كلاهما تحديث الحالة بناءً على حالة سابقة. ومع ذلك، فإنهما يخدمان أغراضًا مختلفة.
useReducer
هو خطاف للأغراض العامة لإدارة انتقالات الحالة المعقدة على جانب العميل. يتم تشغيله عن طريق إرسال الإجراءات وهو مثالي لمنطق الحالة الذي يحتوي على العديد من التغييرات الممكنة والمتزامنة للحالة (على سبيل المثال، معالج معقد متعدد الخطوات).useActionState
هو خطاف متخصص مصمم للحالة التي تتغير استجابةً لإجراء واحد، عادة ما يكون غير متزامن. دوره الأساسي هو التكامل مع نماذج HTML، وإجراءات الخادم، وميزات العرض المتزامن في React مثل انتقالات الحالة المعلقة.
الخلاصة: بالنسبة لإرسال النماذج والعمليات غير المتزامنة المرتبطة بالنماذج، فإن useActionState هو الأداة الحديثة والمصممة لهذا الغرض. بالنسبة لآلات الحالة المعقدة الأخرى من جانب العميل، يظل useReducer خيارًا ممتازًا.
الخاتمة: تبني مستقبل نماذج React
خطاف useActionState هو أكثر من مجرد API جديد؛ إنه يمثل تحولًا أساسيًا نحو طريقة أكثر قوة ووضوحًا وتمركزًا حول المستخدم للتعامل مع النماذج وتغييرات البيانات في React. من خلال اعتماده، فإنك تكتسب:
- تقليل الكود المكرر: خطاف واحد يحل محل استدعاءات useState المتعددة وتنظيم الحالة اليدوي.
- حالات انتظار مدمجة: تعامل بسلاسة مع واجهات المستخدم أثناء التحميل باستخدام الخطاف المصاحب useFormStatus.
- تحسين تدريجي مدمج: اكتب كودًا يعمل مع أو بدون JavaScript، مما يضمن إمكانية الوصول والمرونة لجميع المستخدمين.
- تبسيط الاتصال بالخادم: مناسب بشكل طبيعي لإجراءات الخادم، مما يبسط تجربة تطوير المكدس الكامل (full-stack).
عندما تبدأ مشاريع جديدة أو تعيد هيكلة المشاريع الحالية، فكر في استخدام useActionState. لن يحسن فقط تجربة المطور الخاصة بك عن طريق جعل الكود الخاص بك أكثر نظافة وقابلية للتنبؤ، بل سيمكّنك أيضًا من بناء تطبيقات عالية الجودة أسرع وأكثر مرونة ومتاحة لجمهور عالمي متنوع.