الإقليم ٠٧ — مكبّر الصوت العام
يبني فوق: ألم تمرير props عبر مستوياتٍ لا تستخدمها (١)، الذاكرة والرسم (٢). النطاق: كيف تُوصِل قيمةً لأي عمقٍ في الشجرة بلا تمريرٍ يدوي، وثمن ذلك الحقيقي. يفتح: خريطة المسرح (٨) — أين تعيش هذه القيم في هيكلة مشروعٍ حقيقي.اللغز المستفزّ (استئنافٌ متعمَّد من الإقليم ١)
تذكر اللغز د من الإقليم ١: سلسلة Page → Sidebar → UserPanel → UserBadge، حيث UserBadge وحده يستخدم currentUserName، لكن Sidebar وUserPanel يمرّرانها رغم أنهما لا يستخدمانها إطلاقاً. سجّلتَ يومها شعورك بلا حلّ. الآن جرّب تخيّل السلسلة بعشرة مستويات، وعشر قيمٍ مختلفة (هويّة المستخدم، الثيم، اللغة، صلاحيات...) — كل واحدة تحتاج تمريرها عبر كل مستوى، ولو أضفت مستوىً جديداً في المنتصف يوماً ما، عليك تحديث تمرير كل القيم عبره حتى لو لا يستخدم أياً منها.
السؤال المستفزّ: ماذا لو استطاع
UserBadge"التقاط" القيمة مباشرة من نقطةٍ بعيدة في الشجرة، بلا أن يمرّرها أي مستوىً وسيط إطلاقاً؟
ليش وكيف: نظامٌ بديل — بثٌّ لا تمرير
بدل تمرير القيمة يدوياً عبر كل مستوى، اصنع نقطة بثّ يلتقطها أي أحفادٍ يريدونها، مهما بعُد عمقهم، دون أن يعرف أي مستوىً وسيط بوجودها أصلاً:
type UserContextValue = { currentUserName: string };
const UserContext = createContext<UserContextValue | null>(null); // ١) عرّف القناة
function Page() {
return (
<UserContext.Provider value={{ currentUserName: "أحمد" }}> {/* ٢) ابثّ */}
<Sidebar />
</UserContext.Provider>
);
}
function Sidebar() {
return <UserPanel />; // لا يعرف عن UserContext إطلاقاً
}
function UserPanel() {
return <UserBadge />; // ولا هذا أيضاً
}
function UserBadge() {
const ctx = useContext(UserContext); // ٣) التقط مباشرة، من أي عمق
if (!ctx) throw new Error("UserBadge يجب أن يكون داخل UserContext.Provider");
return <span>{ctx.currentUserName}</span>;
}
Sidebar وUserPanel عادا لبساطتهما الأصلية تماماً — لا أثر لـcurrentUserName في توقيعهما إطلاقاً. لاحظ نمط الحراسة (if (!ctx) throw ...): القيمة الابتدائية لـcreateContext هنا null عمداً — إشارة "لا مزوّد فوقي بعد". لو استُخدم UserBadge خارج أي <UserContext.Provider> (خطأٌ برمجي حقيقي)، تحصل على رسالة خطأٍ واضحة فوراً عند نقطة الاستخدام، بدل انهيارٍ غامض لاحقاً حين تحاول قراءة خاصية من null. هذا النمط شائعٌ لدرجة أنه يُغلَّف عادةً في خطافٍ مخصّص (تذكّر الإقليم ٦):
function useCurrentUser(): UserContextValue {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useCurrentUser يجب أن يُستدعى داخل UserContext.Provider");
return ctx;
}
اللغز/البناء من الصفر — الجزء الأول
اللغز أ. أعد بناء سلسلة Page → Sidebar → UserPanel → UserBadge من الإقليم ١ باستعمال Context كما أعلاه، مع خطاف useCurrentUser الحارس. تحقّق: احذف currentUserName من توقيع Sidebar/UserPanel بالكامل، وتأكّد أن التطبيق لا يزال يعمل. ثم اختبر الحارس عمداً: استعمل UserBadge خارج أي Provider وتأكّد أن الخطأ الذي تحصل عليه واضحٌ ومباشر، لا غامض.
ليش: الثمن — مكبّر صوتٍ يُسمِع الجميع، لا المهتمّين فقط
الاسم "مكبّر الصوت العام" ليس مجازاً زخرفياً. حين تتغيّر قيمة Provider، كل مكوّنٍ يستهلك ذلك الـContext (عبر useContext) يُعاد رسمه، بصرف النظر عمّا إذا كان يستخدم الجزء الذي تغيّر فعلياً أو جزءاً آخر لم يتغيّر. Context لا يملك اشتراكاً انتقائياً على مستوى الحقول — الاشتراك كلّه أو لا شيء.
اختبر هذا بنفسك، بقيمةٍ تجمع أكثر من حقلٍ يتغيّر بمعدّلاتٍ مختلفة تماماً:
type AppContextValue = {
theme: "dark" | "light";
notificationCount: number;
};
const AppContext = createContext<AppContextValue | null>(null);
function AppProvider({ children }: { children: React.ReactNode }) {
const [theme] = useState<"dark" | "light">("dark"); // لا يتغيّر إطلاقاً هنا
const [notificationCount, setNotificationCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setNotificationCount((n) => n + 1), 1000); // كل ثانية
return () => clearInterval(id);
}, []);
return (
<AppContext.Provider value={{ theme, notificationCount }}>
{children}
</AppContext.Provider>
);
}
function ThemeLabel() {
const ctx = useContext(AppContext)!;
console.log("ThemeLabel يُعاد رسمه — يهمّه theme فقط");
return <span>الثيم: {ctx.theme}</span>;
}
شغّلها وراقب الـconsole. ThemeLabel يُعاد رسمه تُطبَع كل ثانية بالضبط — رغم أن theme لم يتغيّر ولو مرّةً واحدة طوال التشغيل! السبب: AppProvider يُعاد رسمه كل ثانية (بسبب notificationCount)، فيُنشئ كائن value جديداً ({ theme, notificationCount } سطرٌ جديد، مرجعٌ جديد) في كل مرّة — وContext يرى "القيمة تغيّرت" (مرجعياً)، فيُعيد رسم كل مستهلكٍ، بلا استثناء ولا تمييز.
اللغز/البناء من الصفر — الجزء الثاني: أصلح الفانوت
اللغز ب. أعد إنتاج المثال أعلاه بنفسك، وأضف عدّاد رسمٍ مرئياً حقيقياً (لا console.log فقط) لـThemeLabel على الشاشة، وتأكّد أنه يتحرّك كل ثانية رغم أن الثيم ثابت. هذا يُثبت المشكلة بعينك، لا بالقراءة فقط.
اللغز ج — الإصلاح: قنواتٌ منفصلة بمعدّل تغيّرها. قسّم AppContext الواحد إلى قناتين منفصلتين تماماً: ThemeContext (نادر التغيّر) وNotificationContext (متكرّر التغيّر)، كل واحدة بمزوّدها الخاص. اجعل ThemeLabel يستهلك ThemeContext فقط. معيار النجاح: كرّر تجربة اللغز ب — يجب أن يتوقّف عدّاد رسم ThemeLabel عن الحركة تماماً، بينما مكوّنٌ آخر يستهلك NotificationContext يستمرّ بالتحرّك كل ثانية كما هو متوقَّع (فهو فعلاً يهتمّ بما يتغيّر).
اللغز د — تثبيتٌ إضافي بـuseMemo (حماية من فانوتٍ غير مقصود). حتى بعد الفصل، لو أُعيد رسم AppProvider لسببٍ لا علاقة له بالثيم (مكوّنٌ أبٌ أعلى تغيّر)، ستُنشأ قيمة { theme } كائناً جديداً كل مرّة رغم أن الثيم لم يتغيّر فعلياً. أضف useMemo(() => ({ theme }), [theme]) من الإقليم ٥ حول قيمة كل Provider. لاحظ الفرق الدقيق بين هذا اللغز واللغز ج: اللغز ج حلّ مشكلة "بيانات لا علاقة لها ببعضها مجمَّعة في قناة واحدة"؛ هذا اللغز يحلّ مشكلة "نفس البيانات، لكن كائنٌ جديدٌ بلا داعٍ في كل رسم". كلاهما ضروري، ولا يُغني أحدهما عن الآخر.
ليش: البثّ نفسه — نموذجٌ مصغّر يكشف لماذا الفانوت حتميّ
لفهم أن الفانوت ليس "قصوراً" في تطبيق React بل خاصيةٌ حتميّة لأي آلية "قيمةٍ مشتركة واحدة يشترك فيها الجميع"، ابنِ أبسط نموذج ممكن لمخزنٍ مشترك:
function createStore<T>(initial: T) {
let value = initial;
const subscribers = new Set<() => void>();
return {
get: () => value,
set: (next: T) => {
value = next;
subscribers.forEach((notify) => notify()); // يُخطر الجميع، بلا استثناء
},
subscribe: (callback: () => void) => {
subscribers.add(callback);
return () => subscribers.delete(callback);
},
};
}
هذا كل ما "قيمةٌ مشتركة" تعنيه في أبسط صورها: مكانٌ واحد، وقائمة كل من يريد أن "يُخطَر". لاحظ توقيع set: لا يوجد أي معامل يقول "أخطر فقط من يهتمّ بهذا الجزء تحديداً" — لأن value كتلةٌ واحدة، لا حقولاً منفصلة الاشتراك. الإخطار الجماعي ليس خياراً تصميمياً يمكن "إصلاحه" بسهولة؛ هو النتيجة المباشرة لكون التخزين موحّداً. Context الحقيقي أعقد (يمشي شجرة الفيبر ليجد أقرب Provider) لكن جوهر الفانوت هو بالضبط هذا: خزّانٌ واحد، إخطارٌ جماعي.
اللغز هـ (اختياري، تعميقي). صِل createStore بمكوّن عبر خطافٍ مخصّص:
function useStore<T>(store: ReturnType<typeof createStore<T>>): T {
const [, forceRerender] = useStateNano(0);
useEffect(() => store.subscribe(() => forceRerender((n) => n + 1)), [store]);
return store.get();
}
اربطه بمثال الثيم/الإشعارات نفسه (theme وnotificationCount في نفس createStore واحد)، وأعد إنتاج نفس الفانوت الذي رأيته في اللغز ب — بلا React Context إطلاقاً هذه المرّة. هذا يثبت أن المشكلة ليست خاصّةً بـ Context — هي خاصّةٌ بأي "قيمةٍ واحدة مشتركة" مهما كانت الأداة.
ليش: حدود الأداة — متى Context هو الخيار الخطأ من الأساس
الآن وقد رأيتَ الثمن بعينك، القاعدة العملية: Context ممتازٌ لقيمٍ نادرة التغيّر يحتاجها عددٌ كبير ومتفرّق من المكوّنات (هويّة المستخدم الحالي، الثيم، اللغة) — الفانوت هنا غير مؤلم لأن التغيّرات نفسها نادرة. لكنه أداةٌ سيّئة لحالةٍ عالية التردّد (موضع مؤشّر الفأرة، نص حقل بحثٍ في كل حرف، عدّاد ثوانٍ) يستهلكها عددٌ كبير من المكوّنات — هناك الفانوت يعني إعادة رسمٍ مستمرّة لكل شيء، لأن Context لا يملك اشتراكاً جزئياً (selector) يسمح لمكوّنٍ أن "يشترك في حقلٍ واحد فقط" من كائنٍ أكبر. هذا القيد بنيوي، لا تفصيلاً يُحَلّ بحيلةٍ إضافية — مكتبات إدارة حالةٍ خارجية (خارج نطاق هذا المنهج عمداً) حلّته عبر اشتراكاتٍ انتقائية حقيقية. اعرف أن هذا موجود؛ لن نبنيه هنا.
الخلاصة — أين تتّصل هذه العقدة بالشجرة
- ما بنيتَه: بثّاً يحلّ ألم prop drilling من الإقليم ١ فعلياً (Provider + useContext + خطاف حارسٍ يرمي خطأً واضحاً)، وقياساً حقيقياً (لا نظرياً) لثمنه (فانوت الرسم)، وإصلاحين متكاملين (تقسيم القنوات بمعدّل التغيّر، وتثبيت هويّة القيمة بـ
useMemo)، ونموذجاً مصغّراً يثبت أن الفانوت حتميّةٌ بنيويةٌ لا عيبٌ في التنفيذ. - العقدة الجديدة: *مشاركة حالةٍ عبر عمق الشجرة، بثمنٍ معروفٍ ومُدار.*
- بابٌ مفتوح: عندك الآن كل أدوات المنطق الحالي (state, effect, ref, memo, reducer, خطافات مخصّصة, context). حان وقت سؤالٍ مختلف تماماً: مشروعٌ فيه عشرات المكوّنات وهذه الأدوات كلها — أين يعيش كل ملف، ومن يملك أي مجلّد، وكيف تمنع الفوضى حين يعمل أكثر من شخص على نفس الشجرة؟ → الإقليم ٨.
الوصلة بمشروعك
AuthContext.tsx وtheme-provider.tsx في مشروعكم هما بالضبط نمط "قيمةٌ نادرة التغيّر يحتاجها الكثير" الذي يناسب Context جيداً: هويّة المستخدم والثيم لا يتغيّران كل ثانية، فالفانوت هنا غير مؤلم عملياً. ولاحظا كلاهما يستعملان نمط الحارس (if (!context) throw new Error(...)) الذي بنيتَه في اللغز أ حرفياً — نفس السطر تقريباً. لو يوماً احتاج المشروع حالةً عالية التردّد (مؤشّر تقدّم تمرين حيّ، عدّاد وقتٍ في IDE مضمَّن) عبر مكوّناتٍ كثيرة، هذا بالضبط الموضع الذي يبدأ فيه التفكير بأداةٍ غير Context.
البذرة التالية: عندنا الآن كل الأدوات المنطقية. لكن لم نتحدّث بعد عن أين تعيش الملفات فعلياً على القرص. مشروعٌ بخمسين مكوّناً وعشرين خطافاً بلا تنظيمٍ واضح يتحوّل لفوضى خلال أسابيع — والتنظيم الصحيح ليس ذوقاً شخصياً، هو قرارٌ يحسم كيف يعمل فريقٌ كامل بلا تصادم.