Prop drilling هو تمرير prop عبر مكوّنات لا تستخدمها، فقط ليتمكن مكوّن أبعد في أسفل الشجرة من قراءتها. هنا تنتقل user من App عبر Page وSidebar لتصل إلى Avatar، المكوّن الوحيد الذي يقرؤها.
أربعة مستويات، ولا يهتم بـ user إلا الأخير. تُظهر Console أن Page وSidebar يعملان من أجل قيمة لا يعرضانها أبدًا. انقر الزر فيسجّلان من جديد.
لماذا يضر
مستويان أو ثلاثة لا بأس بها. لكن prop drilling يصبح مشكلة كلما كبرت الشجرة:
- تحمل المكوّنات الوسيطة props لا تستخدمها. أصبح لدى
PageوSidebarالآن prop باسمuserفي توقيعهما، فلا يمكنك قراءتهما دون أن تتساءل عن الغرض منها. - كل تغيير يمس كل طبقة. أضف
user.avatarUrlأو قيمة ثانية مثلonLogout، فتعدّل كل مكوّن على المسار، لا الطرفين فقط. - نقل مكوّن يكسر السلسلة. انقل
Avatarإلى فرع مختلف وعليك تمريرuserعبر مسار جديد. - من السهل إسقاط prop. انسَ تمريرها في طبقة واحدة فيحصل المكوّن العميق على
undefined، دون أي خطأ في الطبقة التي أضاعتها.
جرّب ذلك: أضف دالة onLogout في App ينبغي أن يستدعيها Avatar. ستعدّل المكوّنات الأربعة كلها.
الحل 1: التركيب مع children
هذا هو الحل الذي يتخطاه الناس. بدلًا من تمرير البيانات إلى الأسفل ليتمكن مكوّن عميق من العرض بها، دع المكوّن الذي يملك البيانات يعرض المكوّن العميق بنفسه، ومرّر العنصر الجاهز إلى الأسفل على أنه children (أو أي prop أخرى). تعرض الطبقات الوسيطة خانة فارغة ولا ترى user أبدًا.
لم يعد Page وSidebar يعرفان بوجود مستخدم. ينشئ App المكوّن Avatar حيث تعيش user ويسلّمه إلى الأسفل كعنصر جاهز. وتصبح مكوّنات التخطيط مثل هذه قابلة لإعادة الاستخدام، وصارت إضافة onLogout تغييرًا في App وAvatar فقط. ما زال كلاهما يسجّل مع كل نقرة، لأن App يمرر إليهما عنصرًا جديدًا في كل مرة يُعرض فيها: التركيب يبسّط الكود، لا عدد مرات العرض. تتناول صفحة children أنماطًا أخرى للخانات.
ينجح التركيب عندما يستطيع المكوّن الذي يملك البيانات أن يقرر أيضًا شكل الجزء العميق. ولا يفيد عندما يكون المكوّن العميق داخل مكتبة أو بنية لا يعرضها المالك.
الحل 2: السياق
عندما تحتاج مكوّنات كثيرة على أعماق مختلفة إلى القيمة نفسها (السمة، أو المستخدم المسجّل، أو اللغة الحالية)، ضعها في السياق. يقرؤها أي مكوّن تحت المزوّد بـ useContext، ولا يتغير شيء بينهما.
يقرأ Avatar وGreeting المستخدم من عمقين مختلفين، ولا يأخذ Page وSidebar أي props. أضف قارئًا ثالثًا في أي مكان تحت Page فيعمل دون لمس الباقي. أما التكلفة: لم يعد تدفق البيانات ظاهرًا في props، ويُعاد عرض كل قارئ عندما تتغير القيمة. تتناول صفحة useContext المزوّدين والقيم الافتراضية وإعادة العرض تلك.
الحل 3: مكتبة حالة
لحالة تطبيق كبيرة تقرؤها مكوّنات كثيرة وتتغير كثيرًا (سلة مشتريات، أو محرر مستندات، أو بيانات مباشرة)، تتيح مكتبة حالة مثل Redux Toolkit أو Zustand أو Jotai لكل مكوّن أن يشترك في الجزء الذي يستخدمه فقط. وهذا يتجنب التمرير عبر الطبقات وتكلفة «كل مستهلك يُعاد عرضه» في سياق واحد كبير. لكنها أيضًا اعتمادية إضافية ومجموعة مفاهيم إضافية، فالجأ إليها عندما يتوقف الحلان الأولان عن الكفاية، لا لإصلاح prop تنتقل ثلاثة مستويات.
// Zustand, for comparison (not available in the editor here)
const useCart = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
function CartCount() {
const count = useCart((s) => s.items.length); // renders only when the count changes
return <span>{count}</span>;
}
متى يكون prop drilling مقبولًا
تمرير props هو طريقة تدفق البيانات في React، وهو أسهل نمط في القراءة: يمكنك تتبع قيمة من الأعلى إلى الأسفل بالنظر إلى JSX. الـ prop التي تمر عبر مكوّن أو اثنين يستخدمان جزءًا منها، أو عبر سلسلة قصيرة من مكوّنات مترابطة، ليست مشكلة تحتاج إلى حل. ارفع الحالة إلى أقرب أب مشترك، ومررها إلى الأسفل، ولا تغيّر النهج إلا عندما تبدأ الطبقات الوسيطة بحمل props لا فائدة لها منها.
الأسئلة الشائعة
ما هو prop drilling في React؟
تمرير prop إلى الأسفل عبر عدة طبقات من المكوّنات التي لا تستخدمها، فقط ليتمكن مكوّن في عمق الشجرة من قراءتها. يقبل كل مكوّن وسيط الـ prop ويمررها.
هل prop drilling سيئ؟
ليس بذاته. تمرير قيمة مستويين أو ثلاثة إلى الأسفل صريح وسهل التتبع. ويصبح مشكلة عندما تحمل طبقات كثيرة props لا تستخدمها أبدًا، فيمس كل تغيير اسم أو حقل جديد ملفات لا علاقة لها به.
كيف أتجنب prop drilling؟
جرّب التركيب أولًا: دع الأب يبني المكوّن العميق ويمرره إلى الأسفل على أنه children أو prop أخرى، فلا ترى الطبقات الوسيطة البيانات أبدًا. وإذا احتاجت مكوّنات كثيرة على أعماق مختلفة إلى القيمة، فاستخدم السياق. وللحالة الكبيرة التي تتغير كثيرًا، فكّر في مكتبة حالة.
هل السياق هو الحل الوحيد لـ prop drilling؟
لا، وكثيرًا ما لا يكون الأفضل. التركيب مع children يزيل التمرير دون إضافة سياق، ويُبقي تدفق البيانات ظاهرًا في الكود.