دليل متعمق لخطاف `useActionState` في React. تعلم إدارة حالات النماذج، واجهات المستخدم قيد الانتظار، والإجراءات غير المتزامنة في تطبيقات React الحديثة.
إتقان `useActionState` في React: الدليل الشامل للتعامل الحديث مع النماذج والإجراءات
في المشهد المتطور باستمرار لتطوير الويب، تواصل React تقديم أدوات قوية تصقل كيفية بناء واجهات المستخدم. إحدى أهم الإضافات الأخيرة، التي عززت مكانتها في React 19، هي خطاف `useActionState`. هذا الخطاف، الذي كان يُعرف سابقًا باسم `useFormState` في الإصدارات التجريبية، هو أكثر بكثير من مجرد أداة مساعدة للنماذج؛ إنه تحول أساسي في كيفية إدارة الحالة المتعلقة بالعمليات غير المتزامنة.
سيأخذك هذا الدليل الشامل من المفاهيم الأساسية إلى الأنماط المتقدمة، موضحًا لماذا يُعد `useActionState` تغييرًا جذريًا في التعامل مع تعديلات البيانات، والتواصل مع الخادم، وتقديم ملاحظات للمستخدم في تطبيقات React الحديثة. سواء كنت تبني نموذج اتصال بسيطًا أو لوحة تحكم معقدة وغنية بالبيانات، فإن إتقان هذا الخطاف سيبسط الكود الخاص بك بشكل كبير ويحسن تجربة المستخدم.
المشكلة الأساسية: تعقيد إدارة حالة الإجراءات التقليدية
قبل أن نتعمق في الحل، دعونا نقدر المشكلة. لسنوات، كان التعامل مع الحالة المحيطة بتقديم نموذج بسيط أو استدعاء API يتضمن نمطًا متوقعًا ولكنه مرهق باستخدام `useState` و `useEffect`. لقد كتب المطورون في جميع أنحاء العالم هذا الكود المتكرر (boilerplate) مرات لا حصر لها.
لنأخذ مثال نموذج تسجيل دخول قياسي. نحن بحاجة إلى إدارة:
- قيم إدخال النموذج (البريد الإلكتروني، كلمة المرور).
- حالة تحميل أو انتظار لتعطيل زر الإرسال وتقديم ملاحظات.
- حالة خطأ لعرض الرسائل من الخادم (على سبيل المثال، "بيانات الاعتماد غير صالحة").
- حالة نجاح أو بيانات من عملية تقديم ناجحة.
مثال 'ما قبل': استخدام `useState`
قد يبدو التنفيذ النموذجي كما يلي:
// A traditional approach without useActionState
import { useState } from 'react';
// A mock API function
async function loginUser(email, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (email === 'user@example.com' && password === 'password123') {
resolve({ success: true, message: 'Welcome back!' });
} else {
reject(new Error('Invalid email or password.'));
}
}, 1500);
});
}
function TraditionalLoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsLoading(true);
setError(null);
try {
const result = await loginUser(email, password);
// Handle successful login, e.g., redirect or show success message
alert(result.message);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
);
}
يعمل هذا الكود، لكن له عدة عيوب:
- الكود المتكرر (Boilerplate): نحتاج إلى ثلاث استدعاءات منفصلة لـ `useState` (`error`، `isLoading`، ولكل حقل إدخال) لإدارة دورة حياة الإجراء.
- الإدارة اليدوية للحالة: نحن مسؤولون عن تعيين `isLoading` يدويًا إلى true، ثم إلى false في كتلة `finally`، ومسح الأخطاء السابقة في بداية كل عملية تقديم جديدة. هذا عرضة للخطأ.
- الاقتران (Coupling): منطق التقديم مرتبط بشدة داخل معالج الحدث الخاص بالمكون.
تقديم `useActionState`: نقلة نوعية في البساطة
`useActionState` هو خطاف React مصمم لإدارة حالة إجراء ما. يتعامل بأناقة مع دورة الانتظار، والإكمال، والخطأ، مما يقلل من الكود المتكرر ويعزز كتابة كود أنظف وأكثر تصريحية.
فهم توقيع الخطاف
صيغة الخطاف بسيطة وقوية:
const [state, formAction] = useActionState(action, initialState);
- `action`: دالة غير متزامنة تقوم بالعملية المطلوبة (مثل استدعاء API، إجراء خادم). تتلقى الحالة السابقة وأي وسائط خاصة بالإجراء (مثل بيانات النموذج) ويجب أن تُرجع الحالة الجديدة.
- `initialState`: قيمة `state` قبل تنفيذ الإجراء على الإطلاق.
- `state`: الحالة الحالية. تحمل `initialState` في البداية، وبعد تشغيل الإجراء، تحمل القيمة التي أرجعها الإجراء. هذا هو المكان الذي ستخزن فيه رسائل النجاح أو تفاصيل الخطأ أو ملاحظات التحقق من الصحة.
- `formAction`: نسخة جديدة ومغلفة من دالة `action` الخاصة بك. تمرر هذه الدالة إلى خاصية `
مثال 'ما بعد': إعادة الهيكلة باستخدام `useActionState`
دعنا نعيد هيكلة نموذج تسجيل الدخول الخاص بنا. لاحظ كم أصبح المكون أنظف وأكثر تركيزًا.
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// The action function is now defined outside the component.
// It receives the previous state and the form data.
async function loginAction(previousState, formData) {
const email = formData.get('email');
const password = formData.get('password');
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1500));
if (email === 'user@example.com' && password === 'password123') {
return { success: true, message: 'Login successful! Welcome.' };
} else {
return { success: false, message: 'Invalid email or password.' };
}
}
// A separate component to show the pending state.
// This is a key pattern for separation of concerns.
function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
function ActionStateLoginForm() {
const initialState = { success: false, message: null };
const [state, formAction] = useActionState(loginAction, initialState);
return (
);
}
التحسينات واضحة على الفور:
- لا إدارة يدوية للحالة: لم نعد ندير حالات `isLoading` أو `error` بأنفسنا. تتولى React هذا داخليًا.
- منطق مفصول (Decoupled): أصبحت دالة `loginAction` الآن دالة نقية وقابلة لإعادة الاستخدام يمكن اختبارها بمعزل عن غيرها.
- واجهة مستخدم تصريحية: يقوم JSX الخاص بالمكون بعرض واجهة المستخدم بشكل تصريحي بناءً على `state` التي يرجعها الخطاف. إذا كانت `state.message` موجودة، فإننا نعرضها.
- حالة انتظار مبسطة: لقد قدمنا `useFormStatus`، وهو خطاف مصاحب يجعل التعامل مع واجهة المستخدم قيد الانتظار أمرًا بسيطًا.
الميزات والفوائد الرئيسية لـ `useActionState`
1. إدارة سلسة لحالة الانتظار مع `useFormStatus`
إحدى أقوى ميزات هذا النمط هي تكامله مع خطاف `useFormStatus`. يوفر `useFormStatus` معلومات حول حالة تقديم النموذج `
async function deleteItemAction(prevState, itemId) {
// Simulate an API call to delete an item
console.log(`Deleting item with ID: ${itemId}`);
await new Promise(res => setTimeout(res, 1000));
const isSuccess = Math.random() > 0.2; // Simulate potential failure
if (isSuccess) {
return { success: true, message: `Item ${itemId} deleted.` };
} else {
return { success: false, message: 'Failed to delete item. Please try again.' };
}
}
function DeletableItem({ id }) {
const [state, deleteAction] = useActionState(deleteItemAction, { message: null });
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(() => {
deleteAction(id);
});
};
return (
Item {id}
{state.message && {state.message}
}
);
}
ملاحظة: عندما لا يتم استخدام `useActionState` داخل `
التحديثات المتفائلة (Optimistic Updates) مع `useOptimistic`
للحصول على تجربة مستخدم أفضل، يمكن دمج `useActionState` مع خطاف `useOptimistic`. تتضمن التحديثات المتفائلة تحديث واجهة المستخدم على الفور، *بافتراض* أن الإجراء سينجح، ثم التراجع عن التغيير فقط إذا فشل. هذا يجعل التطبيق يبدو فوريًا.
لنأخذ قائمة بسيطة من الرسائل. عندما يتم إرسال رسالة جديدة، نريد أن تظهر في القائمة على الفور.
import { useActionState, useOptimistic, useRef } from 'react';
async function sendMessageAction(prevState, formData) {
const sentMessage = formData.get('message');
await new Promise(res => setTimeout(res, 2000)); // Simulate slow network
// In a real app, this would be your API call
// For this demo, we'll assume it always succeeds
return { text: sentMessage, sending: false };
}
function MessageList() {
const formRef = useRef();
const [messages, setMessages] = useState([{ text: 'Hello!', sending: false }]);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(currentMessages, newMessageText) => [
...currentMessages,
{ text: newMessageText, sending: true }
]
);
const formAction = async (formData) => {
const newMessageText = formData.get('message');
addOptimisticMessage(newMessageText);
formRef.current.reset(); // Reset form visually
const result = await sendMessageAction(null, formData);
// Update the final state
setMessages(current => [...current, result]);
};
return (
Chat
{optimisticMessages.map((msg, index) => (
-
{msg.text} {msg.sending && (Sending...)}
))}
);
}
في هذا المثال الأكثر تعقيدًا، نرى كيف يضيف `useOptimistic` الرسالة على الفور مع تسمية "(جارٍ الإرسال...)". ثم يقوم `formAction` بتشغيل العملية غير المتزامنة الفعلية. بمجرد اكتمالها، يتم تحديث الحالة النهائية. إذا فشل الإجراء، ستقوم React تلقائيًا بتجاهل الحالة المتفائلة والعودة إلى حالة `messages` الأصلية.
`useActionState` مقابل `useState`: متى تختار أيهما؟
مع هذه الأداة الجديدة، يبرز سؤال شائع: متى يجب أن أستمر في استخدام `useState`؟
-
استخدم `useState` لـ:
- حالة واجهة المستخدم من جانب العميل فقط والمتزامنة: فكر في تبديل نافذة منبثقة (modal)، أو إدارة علامة التبويب الحالية في مجموعة علامات تبويب، أو التعامل مع المدخلات في المكونات المتحكم بها التي لا تؤدي مباشرة إلى إجراء خادم.
- الحالة التي ليست نتيجة مباشرة لإجراء: على سبيل المثال، تخزين إعدادات الفلتر التي يتم تطبيقها من جانب العميل.
- متغيرات الحالة البسيطة: عداد، علامة منطقية، سلسلة نصية.
-
استخدم `useActionState` لـ:
- الحالة التي يتم تحديثها نتيجة لتقديم نموذج أو إجراء غير متزامن: هذا هو حالة استخدامه الأساسية.
- عندما تحتاج إلى تتبع حالات الانتظار والنجاح والخطأ لعملية ما: يغلف دورة الحياة هذه بأكملها بشكل مثالي.
- التكامل مع إجراءات خادم React: إنه الخطاف الأساسي من جانب العميل للعمل مع إجراءات الخادم.
- النماذج التي تتطلب تحققًا من الصحة وملاحظات من جانب الخادم: يوفر قناة نظيفة للخادم لإرجاع أخطاء التحقق المنظمة إلى العميل.
أفضل الممارسات والاعتبارات العالمية
عند البناء لجمهور عالمي، من الأهمية بمكان مراعاة عوامل تتجاوز وظائف الكود.
إمكانية الوصول (a11y)
عند عرض أخطاء النموذج، تأكد من أنها متاحة لمستخدمي التقنيات المساعدة. استخدم سمات ARIA للإعلان عن التغييرات ديناميكيًا.
// In your form component
const { errors } = state;
// ...
{errors?.email && (
{errors.email}
)}
تشير السمة `aria-invalid="true"` لقارئات الشاشة إلى أن حقل الإدخال به خطأ. يضمن `role="alert"` على رسالة الخطأ أنه يتم الإعلان عنها للمستخدم بمجرد ظهورها.
التدويل (i18n)
تجنب إرجاع سلاسل نصية للأخطاء مكتوبة بشكل ثابت (hardcoded) من إجراءاتك، خاصة في تطبيق متعدد اللغات. بدلاً من ذلك، أرجع رموز أخطاء أو مفاتيح يمكن ربطها بسلاسل نصية مترجمة على العميل.
// Action on the server
async function internationalizedAction(prevState, formData) {
// ...validation logic...
if (password.length < 8) {
return { success: false, error: { code: 'ERROR_PASSWORD_TOO_SHORT' } };
}
// ...
}
// Component on the client
import { useTranslation } from 'react-i18next';
function I18nForm() {
const { t } = useTranslation();
const [state, formAction] = useActionState(internationalizedAction, {});
return (
{/* ... inputs ... */}
{state.error && (
{t(state.error.code)} // Maps 'ERROR_PASSWORD_TOO_SHORT' to 'Password must be at least 8 characters long.'
)}
);
}
سلامة الأنواع مع TypeScript
يوفر استخدام TypeScript مع `useActionState` سلامة أنواع ممتازة، مما يكتشف الأخطاء قبل حدوثها. يمكنك تحديد أنواع لحالة الإجراء والحمولة الخاصة به.
import { useActionState } from 'react';
// 1. Define the state shape
type FormState = {
success: boolean;
message: string | null;
errors?: {
email?: string;
password?: string;
} | null;
};
// 2. Define the action function's signature
type SignupAction = (prevState: FormState, formData: FormData) => Promise;
const signupAction: SignupAction = async (prevState, formData) => {
// ... implementation ...
// TypeScript will ensure you return a valid FormState object
return { success: false, message: 'Invalid.', errors: { email: '...' } };
};
function TypedSignupForm() {
const initialState: FormState = { success: false, message: null, errors: null };
// 3. The hook infers the types correctly
const [state, formAction] = useActionState(signupAction, initialState);
// Now, `state` is fully typed. `state.errors.email` will be type-checked.
return (
{/* ... */}
);
}
الخاتمة: مستقبل إدارة الحالة في React
إن خطاف `useActionState` هو أكثر من مجرد أداة مساعدة؛ إنه يمثل جزءًا أساسيًا من فلسفة React المتطورة. إنه يدفع المطورين نحو فصل أوضح للمسؤوليات، وتطبيقات أكثر مرونة من خلال التحسين التدريجي، وطريقة أكثر تصريحية للتعامل مع نتائج إجراءات المستخدم.
من خلال مركزية منطق الإجراء وحالته الناتجة، يزيل `useActionState` مصدرًا كبيرًا للكود المتكرر والتعقيد من جانب العميل. يتكامل بسلاسة مع `useFormStatus` للحالات قيد الانتظار و `useOptimistic` لتحسين تجارب المستخدم، مشكلاً ثلاثيًا قويًا لأنماط تعديل البيانات الحديثة.
أثناء بناء ميزات جديدة أو إعادة هيكلة الميزات الحالية، فكر في استخدام `useActionState` كلما كنت تدير حالة تنتج مباشرة عن عملية غير متزامنة. سيؤدي ذلك إلى كود أنظف وأكثر قوة ومتوافق تمامًا مع الاتجاه المستقبلي لـ React.