الإقليم ٠٦ — روتينات مخصّصة
يبني فوق: كل الخطافات السابقة (٢-٥) كنداءات على سجلّ فيبر موضعي. النطاق: كيف تُغلّف منطقاً حالياً كاملاً (لا شكل مرئي) في اسمٍ واحد قابل لإعادة الاستخدام عبر مكوّنات لا علاقة بينها. يفتح: خريطة المسرح (٨) — حين تُقرّر أين تعيش هذه الروتينات في هيكلة مشروعك.
اللغز المستفزّ
بنيتَ في الإقليم ٤ نموذج دخولٍ كاملاً: حقول، تحقّق، حالة تحميل/خطأ، معالج إرسال. الآن يُطلَب منك نموذج تسجيل حسابٍ جديد — حقول مختلفة، لكن نفس الشكل المنطقي بالضبط: useState لكل حقل، useState لحالة status، معالج async يضبط loading ثم يحاول ثم يضبط error/success. تكتبه من الصفر مجدداً، بنفس البنية، بأسماء متغيّراتٍ مختلفة فقط.
بعد أسبوع، تجد بقّة في معالجة الأخطاء — رسالة الخطأ لا تُصفَّر عند إعادة المحاولة. تُصلحها في نموذج الدخول. هل تتذكّر أن تُصلح نفس البقّة في نموذج التسجيل؟ هذا نفس درس الإقليم ١ بالضبط (بطاقات المسارات المكرَّرة يدوياً)، لكن هذه المرّة التكرار ليس في الوصف المرئي (JSX) — هو في المنطق الحالي (الحالة، التحقّق، معالجة الأخطاء). حلّ الإقليم ١ (استخرج دالّة مكوّن) لا يصلح هنا مباشرة، لأن ما تريد استخراجه ليس "شكلاً"، بل سلوكاً حالياً كاملاً بلا أي واجهة مرئية خاصة به.
السؤال المستفزّ: إن كان المكوّن "دالّةٌ تُنتج وصفاً"، فما اسم الشيء الذي هو "دالّةٌ تُنتج حالةً وسلوكاً، بلا أي وصفٍ مرئي خاص بها إطلاقاً"؟
ليش: خطافٌ مخصّص = دالّةٌ عادية، تستدعي خطافاتٍ أخرى داخلها
الجواب أبسط ممّا تتوقّع: دالّةٌ عادية جداً، لا تُرجع JSX، فقط تستدعي useState/useEffect/... داخلها وتُرجع ما يهمّ المستدعي (قيماً، دوالاً). الاصطلاح الوحيد: اسمها يبدأ بـuse. هذا الاصطلاح ليس زخرفاً — هو إشارةٌ صريحة (تعتمد عليها أدوات لاحقاً، الإقليم ١١) بأن هذه الدالّة تستدعي خطافاتٍ داخلها، وبالتالي تخضع لنفس قواعد ترتيب الاستدعاء (الإقليم ٢) تماماً كأي خطافٍ مدمج — استدعِها بشرطٍ أو بحلقة، وستُفسد سجلّ الفيبر بنفس الطريقة بالضبط.
كيف: تتبّع الخانات عبر حدود الدوال
هذه النقطة الميكانيكية دقيقة ومهمة: خطافٌ مخصّص لا يملك سجلّ فيبر خاصاً به. حين يُستدعى، يُتابع الكتابة في نفس سجلّ الفيبر الخاص بالمكوّن الذي استدعاه، من نفس المؤشّر المستمر (hookIndex) — تماماً كأن كوده نُسخ ولُصق مكان نداءه. جرّب:
function useToggleNano(initial: boolean): [boolean, () => void] {
const [value, setValue] = useStateNano(initial); // يستهلك خانةً واحدة
const toggle = () => setValue(!value);
return [value, toggle];
}
function SettingsPanel() {
const [showPassword, toggleShowPassword] = useToggleNano(false); // الخانة ٠
const [rememberMe, toggleRememberMe] = useToggleNano(true); // الخانة ١
// ...
}
تتبّع بيدك: أول نداء لـuseToggleNano (من أجل showPassword) يدخل، ينادي useStateNano مرّة واحدة داخله — تلك تحجز الخانة ٠ في فيبر SettingsPanel نفسه (لا فيبر منفصل لـuseToggleNano!). ثم useToggleNano الثاني (من أجل rememberMe) يُستدعى، وينادي useStateNano مرّة أخرى — الخانة ١، لأن hookIndex استمرّ من حيث توقّف، بلا أي معرفة بأنه "دخل وخرج" من دالّةٍ أخرى بينهما. لا تصادم، رغم أن الدالّتين تبدوان متطابقتين تماماً في الكود — لأن الهويّة ليست بالاسم، هي بالموضع في تسلسل الاستدعاء الكامل لتلك الرسمة، عبر كل حدود الدوال.
هذا يفسّر أيضاً لماذا لا يجوز استدعاء خطافٍ مخصّص بشرط: لو useToggleNano الثاني مشروطٌ بقيمةٍ تتغيّر، سيتزحزح كل ما بعده من خاناتٍ بنفس آلية العطب التي شخّصتَها بنفسك في الإقليم ٢ — القاعدة تنتقل عبر حدود الدوال بلا استثناء.
اللغز/البناء من الصفر
اللغز أ. ابنِ useToggleNano كما أعلاه، واستدعه مرّتين في مكوّنٍ واحد لغرضين مختلفين. اكتب بجملةٍ رقم الخانة (hookIndex) التي يحجزها كل نداء، متتبّعاً التسلسل يدوياً كما في الشرح. تحقّق تجريبياً: بدّل القيمة الأولى، وتأكّد أن الثانية لم تتأثّر إطلاقاً.
اللغز ب — useAsync: خطافٌ يخدم حالتي الإقليم ٤ معاً. ابنِ خطافاً مخصّصاً عاماً (generic) بالشكل التالي:
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function useAsync<T>(): {
state: AsyncState<T>;
run: (fn: () => Promise<T>) => void;
};
المطلوب منك تصميمه وبناءه بنفسك (لا حلّ جاهز هنا): state يعكس حالة آخر عملية شُغِّلت عبر run؛ استدعاء run(fn) يضبط الحالة إلى loading، يشغّل fn، ثم يضبطها إلى success/error حسب النتيجة. قيدٌ حاكم: هويّة run يجب أن تبقى ثابتة عبر إعادات الرسم (لماذا؟ سترى في الاستخدام الثاني أدناه — هذا يستعمل مباشرة ما بنيتَه في الإقليم ٥).
استخدمه في سيناريوهين مختلفين تماماً، بلا أي تعديلٍ على الخطاف نفسه:
- معالج حدث (كنموذج الدخول، الإقليم ٤): زر إرسالٍ ينادي
run(() => fakeLogin(email, password))مباشرة داخلonSubmit. - أثر (كجلب قائمة عند الظهور، الإقليم ٣):
useEffect(() => { run(() => fakeFetchCourses()); }, [run]). لاحظ أنrunنفسها الآن ضمن مصفوفة التبعيات — لو لم تُثبّت هويّتها (بـuseCallbackبتبعياتٍ فارغة داخلuseAsyncنفسه)، سيراها الأثر "جديدة" في كل رسم، فيُعاد تشغيله بلا توقّف (نفس حلقة اللغز الاستفزازي في الإقليم ٣، بثوبٍ جديد). تحقّق فعلياً: انزعuseCallbackمن داخل تصميمك مؤقّتاً وراقب الحلقة تعود؛ أعده وراقبها تختفي.
معيار النجاح الكلي: خطافٌ واحدٌ، بلا أي تفرّعٍ داخلي حسب طريقة الاستخدام، يخدم حالتي الإقليم ٤ (فعلٌ مباشر، وتزامنٌ عبر أثر) بلا تناقض. اكتب جملةً تشرح لماذا هذا لا يُبطل درس الإقليم ٤ (متى تستعمل أثراً ومتى معالج حدث) — الخطاف لا يقرّر هذا، المستدعي لا يزال من يقرّر متى ينادي run.
الخلاصة — أين تتّصل هذه العقدة بالشجرة
- ما بنيتَه: فهماً دقيقاً أن الخطاف المخصّص دالّةٌ عادية بلا سجلّ ذاكرةٍ خاص بها — تستعير خانات فيبر المستدعي بالتسلسل. وخطاف
useAsyncعاماً يعيد استخدام درسي الإقليم ٤ (فعل/تزامن) والإقليم ٥ (useCallbackلثبات الهويّة) معاً في أداةٍ واحدة قابلة للاستدعاء من أي مكوّن. - العقدة الجديدة: *إعادة استخدام المنطق الحالي عبر التركيب، لا النسخ.* هذا يُكمل ثنائي الإقليم ١ (مكوّنات تُعيد استخدام الشكل) بنظيره (خطافاتٌ تُعيد استخدام السلوك).
- بابٌ مفتوح: كل ما بنيتَه حتى الآن يعيش داخل شجرة props — بيانات تنزل من أبٍ لابنه مباشرة. لكن ماذا لو احتاج مكوّنٌ بعيدٌ جداً في الشجرة (تذكّر ألم الإقليم ١، اللغز د) قيمةً يملكها جدٌّ بعيد، بلا تمريرٍ يدوي عبر عشرة مستويات؟ خطافٌ مخصّص وحده لا يحلّ هذا — يحتاج آليةً جديدة كلياً. → الإقليم ٧.
الوصلة بمشروعك
useLoginForm.ts وuseRegisterForm.ts في مشروعكم مثالان حقيقيان بالضبط على هذا الدرس: منطقٌ حاليٌّ متشابه الشكل (حقول + تحقّق + حالة تحميل/خطأ)، مُستخرَج كل واحدٍ في خطافه الخاص بدل تكراره داخل مكوّنات LoginDialog/RegisterForm مباشرة. ولاحظ أين تعيش هذه الملفات في هيكلة المشروع: features/auth/hooks/ — مجلَّدٌ منفصلٌ تماماً عن features/auth/components/. لماذا هذا الفصل بالذات مقصودٌ في هيكلة المشروع هو موضوع الإقليم القادم مباشرة.
البذرة التالية: عندنا الآن أدواتٌ لإعادة استخدام الشكل (مكوّنات) والسلوك (خطافات). لكن مشروعاً حقيقياً فيه عشرات الملفات من كليهما — كيف تُنظّمها بحيث يعمل شخصان على نفس المشروع بلا أن يطأ أحدهما ملفات الآخر؟