يواجه كل مطور React في النهاية نفس العقبة. تقوم بجلب كائن مستخدم (user object) داخل مكون App الرئيسي، ثم تمرره للأسفل، ثم للأسفل مرة أخرى. عبر غلاف المسار (route wrapper)، وعبر هيكل التخطيط (layout shell)، وعبر حاوية الشريط الجانبي (sidebar container)، فقط لكي يتمكن مكون صورة رمزية (avatar) صغير موجود في ثلاث طبقات عمقًا من عرض صورة الملف الشخصي. المكونات الموجودة في المنتصف لا تهتم بكائن المستخدم هذا؛ فهي مجرد وسيط لنقل الطرد. هذا هو الـ prop drilling، وهو ما يحول شجرة المكونات النظيفة إلى لعبة "هاتف مكسور" (game of telephone) محبطة.
تبدأ المعاناة الحقيقية عندما يتغير شكل تلك البيانات. ربما يبدأ الجزء الخلفي (backend) في استخدام user.profile.avatar بدلاً من user.avatar. فجأة، تجد نفسك تقوم بتعديل واجهات TypeScript أو PropTypes عبر خمسة ملفات لا تستخدم البيانات نفسها أبدًا. هنا يأتي دور React Context API.
كيف يعيد Context صياغة تدفق البيانات
تخيل Context كجهاز توجيه WiFi (router) يقع في مركز منزلك. بدون وجوده، ستحتاج إلى كابلات Ethernet تمتد عبر كل غرفة للحصول على إشارة لجهاز الكمبيوتر الخاص بك. أما بوجوده، فإن جهاز التوجيه يبث الإشارة عبر الهواء، وأي جهاز يمتلك كلمة المرور الصحيحة يمكنه الاتصال مباشرة. الجدران لا تشكل عائقًا.
بمصطلحات React، يمكن لجذر تطبيقك (root) بث البيانات عبر شجرة المكونات (component tree) دون مطالبة كل طبقة بالعمل كعامل توصيل. يمكن لأي مكون متداخل أن يشترك في هذا البث ويتلقى بالضبط ما يحتاجه.
الأجزاء الثلاثة الأساسية
تتلخص Context API في ثلاثة أجزاء متحركة.
React.createContext() تقوم بإعداد قناة البث. وهي تعيد كائنًا يحتوي على Provider و (في الأكواد القديمة) Consumer. تحتاج إلى استدعاء هذه الدالة مرة واحدة فقط لميزة معينة.
The Provider هو مكون يغلف جزءًا من شجرتك. وهو يقبل خاصية (prop) واحدة تسمى value. وأي شيء تضعه في هذه الخاصية يصبح متاحًا لكل المكونات المنحدرة منه، بغض النظر عن مدى عمقها.
useContext هو الـ Hook الذي يسمح للمكونات الوظيفية (function components) بالاتصال بهذا البث. داخل مكونك، تمرر كائن الـ context الذي أنشأته إلى useContext ليعيد لك القيمة الحالية. هذا كل شيء. لا حاجة لمغلفات، ولا لخصائص إضافية.
قبل ظهور Hooks، كان عليك استخدام نمط Consumer مع render props. كان ذلك يعمل، لكنه كان يتسبب في الكثير من الإزاحات (indentation) وفوضى المغلفات. أما useContext فقد بسط كل ذلك في سطر واحد داخل جسم الدالة.
متى يكون استخدام Context منطقيًا حقًا
لا تلجأ إلى Context لمجرد العادة. فقد صُمم للبيانات التي تشترك فيها العديد من المكونات غير المرتبطة عبر فروع مختلفة من شجرتك. تشمل المرشحات الجيدة ما يلي:
- إعدادات المظهر (Theme settings). ليس فقط الوضع الفاتح أو الداكن، بل أيضًا رموز المسافات (spacing tokens)، ولوحات الألوان، ومقاييس الخطوط. تمرير هذه الإعدادات يدويًا عبر كل زر أو نافذة منبثقة (modal) يصبح أمرًا مرهقًا بسرعة.
- مصادقة المستخدم (User authentication). حالة تسجيل الدخول، أو مصفوفة الصلاحيات، أو كائن المستخدم الحالي. قد يتواجد شريط الرأس (header bar)، وأداة لوحة التحكم (dashboard widget)، وحارس المسارات الخاصة (private route guard) في زوايا مختلفة تمامًا من الشجرة.
- تفضيلات اللغة (Language preferences). سلاسل النصوص المحلية (Locale strings)، وتنسيقات التاريخ، ورموز العملات. المكونات الطرفية العميقة مثل تسميات النماذج (form labels) تحتاج إلى هذه البيانات دون الحاجة لأن يعرف كل أب في المسار عنها.
- بيانات عربة التسوق (Shopping cart data). عدد العناصر، والقيمة الإجمالية، ووظائف الإضافة إلى العربة. يحتاج شارة الرأس وصفحة الدفع إلى نفس الحالة، لكنهما عادة ما يقعان تحت فروع تخطيط مختلفة تمامًا.
مبدل مظهر عملي
أحد أوضح الطرق لرؤية Context أثناء العمل هو تبديل المظهر (theme toggle). إليك كيف يمكنك إعداده دون إغفال التفاصيل المهمة حقًا.
أولاً، قم بإنشاء ملف ThemeContext.js. استدعِ React.createContext() وخزن النتيجة. ثم قم ببناء مكون ThemeProvider الذي يدير المظهر الحالي باستخدام useState أو useReducer. قم بتغليف العناصر الأبناء (children) في الـ Provider الخاص بالـ context، ومرر كائنًا يحتوي على كل من المظهر الحالي ووظيفة لتبديله. قم بتصدير (export) كل من ThemeProvider وكائن الـ context نفسه.
ثانيًا، اذهب إلى نقطة الدخول في تطبيقك. استورد ThemeProvider وقم بتغليف تطبيقك بالكامل به. إذا تخطيت هذه الخطوة، فإن أي شيء يحاول قراءة الـ context لاحقًا لن يرى سوى القيمة الافتراضية.
ثالثًا، داخل مكون Header أو Content ، استورد كائن الـ context و useContext. استدعِ الـ Hook، وقم بتفكيك (destructure) المظهر ووظيفة التبديل، وقم بتطبيق فئات CSS الخاصة بك بشكل مشروط. أضف زرًا يستدعي وظيفة التبديل. لن يتلقى المكون خاصية theme من أبيه أبدًا؛ بل سيسحب الإشارة مباشرة من الهواء.
Prop Drilling، أم Context، أم Redux؟
الاختيار بين هذه الأدوات لا يتعلق بالولاء بقدر ما يتعلق بشكل الحالة (state) الخاصة بك.
Prop drilling أمر جيد تماماً لمستويين أو ثلاثة من العمق. فهو صريح، وسهل التتبع في بيئة التطوير (IDE)، ويجعل التبعيات واضحة. لا تظهر المشكلات إلا عندما تبدأ في تمرير نفس الـ prop عبر ست أو سبع طبقات.
تأتي Context API مدمجة مع React نفسه. وهذا يعني عدم وجود حجم إضافي للحزمة (bundle size) وعدم الحاجة لإعدادات خارجية. وهي تتعامل مع الحالة العامة (global state) الصغيرة إلى المتوسطة بشكل رائع، خاصة البيانات التي تتغير بشكل غير متكرر مثل السمات (themes) أو ملفات تعريف المستخدمين.
تتطلب Redux تثبيت مكتبات إضافية وكتابة الكثير من الأكواد النمطية (boilerplate). لكنها تؤتي ثمارها عندما يكون منطق الحالة (state logic) معقداً، أو عندما تتفاعل أجزاء متعددة من الحالة بطرق عميقة، أو عندما تحتاج إلى تصحيح الأخطاء عبر السفر عبر الزمن (time-travel debugging) والبرمجيات الوسيطة (middleware). بالنسبة للبيانات العامة البسيطة، تعتبر Redux مبالغاً فيها (overkill).
واقع الأداء الذي لا يتحدث عنه أحد
إليك الثغرة التي تميز التنفيذ من قبل المبتدئين (junior) عن المحترفين (senior). عندما تتغير قيمة Context Provider، فإن كل مكون يستهلك ذلك السياق (context) يعيد عملية الصيرورة (re-renders). لا يهم إذا كان الجزء المحدد الذي يهتم به المكون قد ظل كما هو؛ فـ React يرى المرجع (reference) الجديد ويجدول عملية تحديث.
إذا وضعت حالة تطبيقك بالكامل في StoreContext واحد ضخم، فأنت فعلياً قد ربطت واجهة المستخدم بالكامل ببعضها البعض. تغيير إعداد السمة (theme) سيؤدي إلى إعادة صيرورة عربة التسوق، ورسوم لوحة التحكم البيانية، وقائمة الإشعارات. هذا عمل غير ضروري.
قم بتقسيم السياقات (contexts) حسب النطاق (domain). احتفظ بـ ThemeContext للإعدادات المرئية، و UserContext لبيانات الملف الشخصي، و CartContext لحالة التجارة. إذا قام المستخدم بتعديل اسمه المعروض، فسيتم تحديث الترويسة (header) دون المساس بشبكة المنتجات. أيضاً، كن حذراً بشأن ما تمرره إلى خاصية value في الـ Provider. إذا مررت كائناً حرفياً (object literal) مثل { theme, toggleTheme } مباشرة أثناء الصيرورة، فستنشئ مرجعاً جديداً في كل عملية صيرورة مما يؤدي إلى تحديثات لا داعي لها. قم بتثبيت هذا الشكل باستخدام useMemo إذا كانت القيمة تحتوي على وظائف (functions) أو بيانات غير بدائية (non-primitive data).
أخطاء تهدر الساعات
هناك خطآن يقع فيهما الفرق مراراً وتكراراً.
نسيان تصدير كائن السياق (context object). من السهل تصدير مكون ThemeProvider ثم محاولة استدعاء useContext(ThemeProvider). ليس هذا هو طريقة العمل؛ فالمخطاف (Hook) يحتاج إلى كائن السياق الذي تعيده createContext وليس المكون المغلف (wrapper component). إذا قمت بتصدير الـ Provider فقط، فلن يجد المستهلكون (consumers) شيئاً لاستيراده.
استدعاء useContext خارج الـ Provider الخاص به. يعيد الـ Hook القيمة الافتراضية التي مررتها إلى createContext. إذا لم تمرر قيمة افتراضية، فستحصل على undefined. إذا كانت شجرة المكونات تقوم بصيرورة المستهلك في مكان أعلى في الـ DOM من الـ Provider، أو إذا كان الـ Provider مفقوداً تماماً، فلن تصل بياناتك ببساطة. تأكد مرتين من أن ملف الـ index أو الـ root يقوم فعلياً بتغليف التطبيق.
الخلاصة الحقيقية
React Context ليس ثورة في إدارة الحالة. إنه أداة مستهدفة لمشكلة مكانية محددة: إيصال البيانات إلى المكونات البعيدة دون تحويل كل طبقة إلى مكتب بريد. استخدمه للبيانات العالمية حقاً، وحافظ على تقسيم السياقات حسب النطاق لحماية أداء الصيرورة، وقم دائماً بتغليف شجرتك بالـ Provider الصحيح قبل محاولة قراءة الإشارة. أتقن هذه العادات، وستظل أشجار المكونات لديك نظيفة، وسريعة، وسهلة الفهم.
