تمرير الخصائص (props) عبر طبقات من المكونات غير المهتمة بها هو عمل شاق وممل. في يوم ما تكون مشغولاً بإطلاق ميزة جديدة، وفي اليوم التالي تجد نفسك تعدل ستة ملفات فقط لإعادة تسمية خاصية واحدة. هذا هو "تمرير الخصائص" (prop drilling) باختصار: يمتلك الأب بيانات، ويحتاجها ابن عميق، ويصبح كل مكون بينهما مجرد ساعي بريد. يظل التطبيق يعمل، لكن قاعدة الكود تصبح هشة؛ فإذا حذفت طبقة وسيطة، سينهار نصف هيكل المكونات، وإذا غيرت نوع بيانات (type)، سيعترض TypeScript عبر ثلاث مجلدات مختلفة. وُجدت React Context API لإزالة هؤلاء الوسطاء تماماً.
ماذا يعني تمرير الخصائص (Prop Drilling) في الواقع
تخيل هيكل تطبيق قياسي. لديك مكون App يقوم بجلب بيانات المستخدم الحالي. داخل App يوجد مكون Layout، وداخل Layout يوجد Sidebar، وداخل Sidebar يوجد Navigation، وأخيراً داخل Navigation تجد UserAvatar الذي يحتاج فعلياً إلى كائن المستخدم (user object).
ينتهي بك الأمر بكود يشبه هذا:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
لا تفعل مكونات Layout و Sidebar و Navigation أي شيء بكائن المستخدم هذا سوى تمريره للأسفل. إنها تتراكم لديها خصائص لا تملكها، وتتضخم واجهاتها (interfaces)، ويتطلب اختبارها محاكاة بيانات (mocking) لا تلمسها أبداً. الجريمة الحقيقية تكمن في مدى سرعة انتشار هذا الأمر؛ فبمجرد إضافة علامة isLoggedIn أو سلسلة نصية للغة locale أو قيمة للمظهر theme، يتكرر نفس المشهد الممل.
كيف يغير Context API قواعد اللعبة
فكر في Context API مثل جهاز توجيه الواي فاي (WiFi router). بدلاً من تمديد كابلات طويلة عبر كل غرفة للوصول إلى كل جهاز، يرسل جهاز التوجيه إشارة عبر الهواء، ويمكن لأي جهاز في النطاق الاتصال مباشرة. بمصطلحات React، جهاز التوجيه هو المزود (Provider)، والإشارة هي الحالة (state) أو البيانات الخاصة بك، والجهاز هو أي مكون متفرع يستدعي useContext.
يتكون الإعداد من ثلاثة أجزاء متحركة:
React.createContext()يبني قناة البيانات.- الـ Provider يغلف جزءاً من شجرة المكونات وينقل القيمة.
- خطّاف
useContextيسمح للعناصر المتفرعة باستقبال تلك القيمة دون المساس بالخصائص الوسيطة.
لا تزال تملك شجرة مكونات، لكن الأغصان بين الجذر والأوراق لم تعد بحاجة إلى الاتفاق على "عقد ساعي بريد".
بناء Context من الصفر
لنقم ببناء مثال ملموس باستخدام إعدادات المظهر (theme)، بما أن معظم التطبيقات تحتاج إلى الوضع الفاتح أو الداكن في مرحلة ما.
أولاً، أنشئ كائن الـ context. هذا هو الأنبوب:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
بعد ذلك، قم بتغليف تطبيقك بالمزود (provider). عادة ما يحدث هذا بالقرب من الجذر (root):
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
الآن يمكن لأي عنصر متفرع التقاط الإشارة مباشرة. إليك زر تبديل مدفون في عمق واجهة المستخدم:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
لاحظ أن Layout و Sidebar و Navigation لا يرون خاصية الـ theme أبداً. إنها تعمل بشكل طبيعي، بينما يقوم ThemeToggle بجلب ما يحتاجه مباشرة من الـ context. التوصيلات غير مرئية من الخارج، وهذا هو الهدف تماماً.
أين يناسب Context الاستخدام فعلياً
يعمل Context بشكل أفضل مع البيانات التي تشترك فيها العديد من المكونات البعيدة ولكن لا يمتلكها أي أب بشكل مباشر ونظيف. تشمل المرشحات الجيدة ما يلي:
- إعدادات المظهر (Theme settings) مثل الوضع الفاتح أو الداكن، أو ألوان التمييز، أو حجم الخط.
- حالة المصادقة (Authentication state) مثل كائن المستخدم الحالي، أو حالة تسجيل الدخول، أو انتهاء صلاحية الجلسة.
- اللغة والموقع (Language and locale) لإعدادات التدويل (internationalization).
- بيانات عربة التسوق التي يجب أن تظل متزامنة عبر شارة الرأس (header badge)، وقائمة عربة التسوق المصغرة، وصفحة الدفع.
قاوم الرغبة في إلقاء كل قطعة من الحالة المحلية (local state) داخل Context. فمدخل نموذج (form input) على بعد مستويين لا يحتاج إلى بث عالمي. احتفظ بـ Context للاهتمامات المشتركة الحقيقية عبر النظام، واترك الباقي كخصائص (props) عادية.
فخاخ الأداء وكيفية تجنبها
الـ Context ليس مجانياً من حيث الأداء. عندما تتحدث قيمة الـ context، يتم إعادة رندرة (re-render) كل مكون مرتبط بهذا الـ context، حتى لو لم تتغير جزئية القيمة التي يهمه أمرها. الخطأ الكلاسيكي هو تمرير كائن حرفي (object literal) جديد إلى الـ Provider عند كل عملية رندرة للأب.
في مثال المظهر الخاص بنا، في كل مرة يعاد فيها رندرة ThemeProvider لأن والده قد تحدّث، فإن التعبير { theme, setTheme } ينشئ كائناً جديداً تماماً. يرى React مرجعاً جديداً، فيتم تحديث كل المستهلكين. إذا كان مظهر تطبيقك نادراً ما يتغير ولكن حالة التطبيق تتغير كثيراً، فأنت تدفع ضريبة عمليات رندرة لا تحتاجها.
الحل يتكون من شقين:
قم بتقسيم الـ contexts الخاصة بك بناءً على تكرار التحديث. لا ينبغي لـ UserContext الذي يتغير مرة واحدة عند تسجيل الدخول أن يتشارك المزود مع NotificationContext الذي يتحدث كل بضع ثوانٍ. اجعلهم منفصلين حتى لا تضطر البيانات الثابتة لركوب نفس قطار إعادة الرندرة الخاص بالبيانات المتطايرة.
غلف القيمة باستخدام useMemo عندما تكون القيمة عبارة عن كائن أو مصفوفة. امنح React مرجعاً مستقراً:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
الآن، تتغير هوية الكائن فقط عندما تتغير قيمة theme فعلياً. أما المكونات الفرعية التي تهتم بالسياق ولكنها محمية بواسطة React.memo في مستويات أدنى، فستتخطى عملية إعادة التصيير.
أخطاء تضيع وقتك
الخطآن اللذان لا يزالان يظهران في كود الإنتاج (production code) من السهل تجنبهما.
أولاً، نسيان تصدير (export) السياق نفسه. إذا قمت بتصدير غلاف الـ Provider فقط وأبقيت كائن السياق خاصاً، فلن يتمكن المطور الذي يكتب ميزة جديدة من استدعاء useContext دون إعادة هيكلة (refactoring) الوحدة الخاصة بك. قم بتصدير السياق حتى يتمكن المستخدمون من استيراد كل من الـ provider و الـ consumer hook بشكل نظيف.
ثانياً، استدعاء useContext خارج الـ Provider المقابل. إذا تم تصيير ThemeToggle في فرع من الشجرة غير مغلف بـ ThemeProvider ، فستعيد الـ hook القيمة الافتراضية الممررة إلى createContext ، أو undefined إذا لم تمرر أي شيء. يؤدي هذا إلى فشل صامت مثل cannot read property of undefined. يمكنك الحماية من ذلك عن طريق تعيين قيمة افتراضية منطقية أو إلقاء خطأ واضح في وقت مبكر من استدعاء الـ hook.
Context مقابل Redux: حافظ على البساطة
لست بحاجة دائماً إلى Redux. بالنسبة للمشاريع متوسطة الحجم، فإن Context مقترناً بـ useState أو useReducer يغطي معظم عمليات مشاركة الحالة (state sharing). يتألق Redux عندما تحتاج إلى تصحيح الأخطاء عبر السفر عبر الزمن (time-travel debugging)، أو برمجيات وسيطة (middleware) معقدة، أو معاملات عالمية (global transactions) يجب التراجع عنها بالتسلسل. إذا كانت حالة تطبيقك بالكامل عبارة عن كائن مستخدم، وسلسلة نصية للثيم، ومصفوفة عربة تسوق، فإن مكتبة store ستضيف كوداً زائداً (boilerplate) لن تستفيد منه أبداً.
ومع ذلك، فإن Context ليس نظاماً كاملاً لإدارة الحالة بمفرده. فهو لا يمنحك لقطة عالمية واحدة (single global snapshot)، ولا يقوم بتجميع التحديثات عبر السياقات غير المرتبطة. استخدمه كبديل لـ prop drilling، وليس كنظام تشغيل لطبقة البيانات بالكامل لديك.
الخلاصة الحقيقية
توقف عن تمرير الـ props عبر المكونات التي لا تهتم بها. أنشئ سياقات مركزة للبيانات التي تمتد فعلياً عبر الشجرة، وقم بتغليف الـ providers في مستوى عالٍ بما يكفي لتغطية المستهلكين (consumers)، وقم دائماً بتثبيت (stabilize) كائن القيمة عندما تمرر مجموعات أو وظائف. تحافظ Context API على كود React الخاص بك مباشراً: تظل الـ props محلية، وتنتقل البيانات العالمية "لاسلكياً"، وتظل حدود المكونات لديك نظيفة.
