افتح أي قاعدة كود React تقريبًا وستلاحظ نفس السلوك التلقائي. يحتاج المطور إلى تتبع قيمة ما، فيلجأ إلى useState. هل تحتاج إلى عداد؟ useState. قيمة إدخال مؤقتة؟ useState. قيمة منطقية (boolean) لتبديل نافذة منبثقة (modal)؟ useState. وقبل مرور وقت طويل، يصبح المكون الواحد يحتوي على عشرات الـ hooks المنفصلة، كل منها يدير جزءًا صغيرًا من البيانات التي قد تحتاج أو لا تحتاج إلى البقاء عبر عمليات الـ render. النتيجة هي كود مشتت، وعمليات إعادة رندر (re-renders) إضافية، وحالة (state) مبعثرة مثل العملات المعدنية المتناثرة عبر المكون.

هذه العادة مفهومة؛ فـ useState هو أول hook يتعلمه معظمنا، وهو يعمل بشكل جيد. لكن "العمل" لا يعني بالضرورة "الملاءمة". فمعاملة كل قطعة بيانات كحالة تفاعلية (reactive state) تخلق مشكلات لا تظهر إلا عندما يكبر حجم المكون.

مجرد تغير القيمة لا يعني أنها بحاجة إلى State

ليست كل متغير يتغير بمرور الوقت ينتمي إلى useState. فبعض القيم هي مجرد نتيجة لشيء آخر تملكه بالفعل. إذا قمت بتخزين الاسم الكامل للمستخدم في الـ state لمجرد أنك قمت بدمج firstName و lastName معًا، فقد أصبح لديك الآن مصدران للحقيقة (sources of truth). عندما يتم تحديث firstName بسبب إعادة رندر للمكون الأب، ستظل حالة fullName قديمة (stale) حتى تقوم بتشغيل effect آخر لمزامنتها. أنت لا تحتاج إلى effect للمزامنة، بل تحتاج إلى قيمة مشتقة (derived value).

const fullName = `${firstName} ${lastName}`;

قم بحسابها أثناء عملية الـ render. إذا كانت عملية الاشتقاق مكلفة، استخدم الـ memoization لها. ولكن لا تمنحها hook خاصًا من نوع useState إلا إذا كان بإمكان المستخدم تعديل الاسم الكامل بشكل مستقل عن أجزائه.

تنطبق القاعدة نفسها على القوائم المفلترة. إذا كنت تحتفظ بكل من allItems و filteredItems في الـ state، فقد ضاعفت مساحة الصيانة (maintenance surface) لديك. قم بالفلترة أثناء الـ render. احتفظ بمصفوفة المصدر ونص الفلتر في الـ state، ثم اشتق القائمة المرئية. هذا يضمن أن القائمة المفلترة لن تخرج أبدًا عن المزامنة مع المصدر.

بعض القيم لا ينبغي أبدًا أن تسبب إعادة رندر (Re-renders)

وُجد useState خصيصًا لإخبار React بأن شيئًا ما قد تغير وأن الـ DOM قد يحتاج إلى تحديث. إذا تغيرت قيمة ما ولكن لم يهتم أي جزء من واجهة المستخدم (UI) بهذا التغيير، فإن useRef هو الأداة الأفضل.

المؤقتات والفترات الزمنية (Timers and intervals) هي المثال الكلاسيكي. تخزين معرفات setInterval في الـ state يسبب إعادة رندر في كل مرة تبدأ فيها مؤقتًا أو توقفه، على الرغم من أن المستخدم لا يمكنه رؤية معرف الفترة الزمنية. يحتفظ الـ ref بهذه القيمة دون إخطار React. ينطبق المنطق نفسه على تتبع الـ props السابقة، أو قياس عقد الـ DOM قبل الرسم (paint)، أو تخزين آخر callback لـ custom hook. اسأل نفسك: هل تحتاج هذه القيمة إلى الظهور على الشاشة؟ إذا كانت الإجابة لا، فمن المحتمل أنها لا تحتاج إلى useState.

عقد الـ DOM نفسها تنتمي أيضًا إلى الـ refs. بينما يمكنك تخزين عنصر DOM في الـ state، فإن القيام بذلك يؤدي إلى إعادة رندر بعد تشغيل الـ ref callback. في معظم الحالات، تحتاج إلى العقدة فقط لاستخدام طريقة imperative أو لإجراء قياس، وليس لرسمها بشكل مختلف.

فخ القيم المنطقية (The Boolean Trap)

تميل اهتمامات واجهة المستخدم المرتبطة إلى التوسع عندما يحصل كل flag على hook خاص به. سترى مكونات تحتوي على isLoading و isError و isSuccess معرفة كثلاث قيم منطقية منفصلة. المشكلة هي أن هذه الحالات الثلاث ليست مستقلة. إذا كانت isLoading و isSuccess كلاهما true في نفس الوقت، فإن واجهة المستخدم الخاصة بك ستكون في حالة مستحيلة، ومع ذلك سيسمح لك TypeScript و React برسمها على أي حال.

تجميع الحالة المرتبطة يمنع هذه التوليفات غير الصالحة. بدلاً من ثلاث قيم منطقية، تتبع سلسلة نصية واحدة للحالة (status string): 'idle' أو 'loading' أو 'success' أو 'error'. يمكن لواحد فقط أن يكون نشطًا في وقت واحد، مما يلغي الحالات المستحيلة على مستوى النوع (type level). إذا كانت البيانات أكثر تعقيدًا، فإن استخدام كائن يحتوي على discriminated union سيجعل الأمور أكثر ترتيبًا. عندما تجد نفسك تقوم بتحديث عدة استدعاءات useState داخل نفس معالج الأحداث (event handler)، فهذه إشارة إلى أن تلك القيم تنتمي إلى بعضها البعض.

استخدم useReducer، وليس useState أخرى

هناك نقطة تصبح فيها تحديثات الحالة مثل لعبة "اضرب الخلد" (whack-a-mole). تقوم باستدعاء setA ثم setB ثم setC بشكل مشروط، وكل ذلك داخل دالة واحدة. يجب على المطور التالي الذي يقرأ هذا الكود تتبع التسلسل لفهم ما يفعله المكون فعليًا.

هنا يبرز دور useReducer. هو لا يحل محل useState لأنه أكثر تقدمًا، بل يحل محلها لأن المنطق يتطلب ذلك. يقوم الـ reducer بمركزية كيفية تغير الحالة. بدلاً من نثر الأوامر (imperatives) عبر معالجات الأحداث، تقوم بإرسال نية (dispatch an intention): dispatch({ type: 'submitted' }). يقرر الـ reducer كيف ستبدو الحالة التالية. هذا يجعل الاختبار سهلاً للغاية، لأن منطق الحالة الخاص بك هو دالة نقية (pure function). كما يجعل تصحيح الأخطاء (debugging) أسهل، لأن كل تغيير يترك إجراءً (action) يمكن تتبعه.

لست بحاجة إلى Redux لتبرير استخدام reducer. إذا كان لديك ثلاثة متغيرات حالة (state variables) أو أكثر يتم تحديثها معاً، أو إذا كانت الحالة التالية تعتمد بشكل كبير على الحالة السابقة، فإن الـ reducer يبسط المكون (component) بشكل كبير.

أين توجد الحالة (State) فعلياً

أحياناً لا تكمن المشكلة في كيفية تخزين الحالة، بل في مكان تخزينها. من الأخطاء الشائعة رفع الحالة (hoisting state) إلى مكون أب (parent) لمجرد أنها قد تكون مطلوبة في مكان آخر. إذا كان هناك مكون طرفي (leaf component) واحد فقط يستخدم جزءاً من الحالة، فاحتفظ بها هناك. هذا هو الـ colocation، وهو يقلل من نطاق تأثير التغييرات (blast radius). لا تجعل المكون الأب يعيد عملية الصيرورة (re-render) لمجرد أن مكوناً فرعياً قد فُتح