نظرة متعمقة في بنية مكونات React، مقارنة بين التركيب والوراثة. تعلم لماذا تفضل React التركيب واستكشف أنماطًا مثل HOCs و Render Props و Hooks لبناء مكونات قابلة لإعادة الاستخدام وقابلة للتطوير.
بنية مكونات React: لماذا ينتصر التركيب على الوراثة
في عالم تطوير البرمجيات، تعتبر الهندسة المعمارية ذات أهمية قصوى. فالطريقة التي ننظم بها التعليمات البرمجية تحدد قابليتها للتطوير والصيانة وإعادة الاستخدام. بالنسبة للمطورين الذين يعملون مع React، فإن أحد أهم القرارات المعمارية يدور حول كيفية مشاركة المنطق وواجهة المستخدم بين المكونات. وهذا يقودنا إلى نقاش كلاسيكي في البرمجة الشيئية، أعيد تصوره لعالم React القائم على المكونات: التركيب مقابل الوراثة.
إذا كنت قادمًا من خلفية في لغات البرمجة الشيئية الكلاسيكية مثل Java أو C++، فقد تشعر أن الوراثة هي الخيار الأول الطبيعي. إنه مفهوم قوي لإنشاء علاقات "هو-نوع-من". ومع ذلك، تقدم وثائق React الرسمية توصية واضحة وقوية: "في Facebook، نستخدم React في آلاف المكونات، ولم نجد أي حالات استخدام نوصي فيها بإنشاء تسلسلات هرمية لوراثة المكونات."
ستقدم هذه المقالة استكشافًا شاملاً لهذا الخيار المعماري. سنقوم بتفكيك ما تعنيه الوراثة والتركيب في سياق React، ونوضح سبب كون التركيب هو النهج الاصطلاحي والأفضل، ونستكشف الأنماط القوية - من المكونات ذات الترتيب الأعلى إلى الخطافات الحديثة - التي تجعل التركيب أفضل صديق للمطور لبناء تطبيقات قوية ومرنة لجمهور عالمي.
فهم الحرس القديم: ما هي الوراثة؟
الوراثة هي ركيزة أساسية للبرمجة الشيئية (OOP). فهي تسمح لفئة جديدة (الفئة الفرعية أو الابن) باكتساب خصائص وأساليب فئة موجودة (الفئة العليا أو الأصل). وهذا يخلق علاقة "هو-نوع-من" وثيقة الترابط. على سبيل المثال، GoldenRetriever
هو نوع من Dog
، والذي هو نوع من Animal
.
الوراثة في سياق غير React
دعونا نلقي نظرة على مثال بسيط لفئة JavaScript لترسيخ المفهوم:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Calls the parent constructor
this.breed = breed;
}
speak() { // Overrides the parent method
console.log(`${this.name} barks.`);
}
fetch() {
console.log(`${this.name} is fetching the ball!`);
}
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy barks."
myDog.fetch(); // Output: "Buddy is fetching the ball!"
في هذا النموذج، تحصل الفئة Dog
تلقائيًا على الخاصية name
والطريقة speak
من Animal
. يمكنها أيضًا إضافة طرقها الخاصة (fetch
) وتجاوز الطرق الموجودة. هذا يخلق تسلسلًا هرميًا جامدًا.
لماذا تتعثر الوراثة في React
في حين أن نموذج "هو-نوع-من" هذا يعمل مع بعض هياكل البيانات، إلا أنه يخلق مشاكل كبيرة عند تطبيقه على مكونات واجهة المستخدم في React:
- الاقتران الوثيق: عندما يرث مكون من مكون أساسي، فإنه يصبح مقترنًا بإحكام بتنفيذ الأصل. يمكن أن يؤدي التغيير في المكون الأساسي إلى تعطيل العديد من المكونات الفرعية في السلسلة بشكل غير متوقع. وهذا يجعل إعادة البناء والصيانة عملية هشة.
- مشاركة المنطق غير المرنة: ماذا لو كنت تريد مشاركة جزء معين من الوظائف، مثل جلب البيانات، مع المكونات التي لا تتناسب مع نفس التسلسل الهرمي "هو-نوع-من"؟ على سبيل المثال، قد يحتاج كل من
UserProfile
وProductList
إلى جلب البيانات، ولكن ليس من المنطقي أن يرثا منDataFetchingComponent
مشترك. - جحيم تمرير الخصائص: في سلسلة وراثة عميقة، يصبح من الصعب تمرير الخصائص من مكون عالي المستوى إلى مكون فرعي متداخل بعمق. قد تضطر إلى تمرير الخصائص من خلال المكونات الوسيطة التي لا تستخدمها حتى، مما يؤدي إلى تعليمات برمجية مربكة ومتضخمة.
- مشكلة "الغوريلا-الموزة": تصف مقولة شهيرة لخبير OOP جو أرمسترونج هذه المشكلة تمامًا: "أردت موزة، ولكن ما حصلت عليه هو غوريلا تحمل الموزة والغابة بأكملها." مع الوراثة، لا يمكنك فقط الحصول على قطعة الوظائف التي تريدها؛ أنت مضطر لإحضار الفئة العليا بأكملها معها.
بسبب هذه المشكلات، قام فريق React بتصميم المكتبة حول نموذج أكثر مرونة وقوة: التركيب.
احتضان طريقة React: قوة التركيب
التركيب هو مبدأ تصميم يفضل علاقة "يملك-نوع-من" أو "يستخدم-نوع-من". بدلاً من أن يكون المكون هو مكونًا آخر، فإنه يملك مكونات أخرى أو يستخدم وظائفها. تُعامل المكونات كوحدات بناء - مثل مكعبات LEGO - يمكن دمجها بطرق مختلفة لإنشاء واجهات مستخدم معقدة دون أن تكون مقفلة في تسلسل هرمي جامد.
نموذج التركيب في React متعدد الاستخدامات بشكل لا يصدق، ويتجلى في العديد من الأنماط الرئيسية. دعونا نستكشفها، من الأساسية إلى الأكثر حداثة وقوة.
التقنية 1: الاحتواء باستخدام `props.children`
أبسط أشكال التركيب هو الاحتواء. هذا هو المكان الذي يعمل فيه المكون كحاوية عامة أو "صندوق"، ويتم تمرير محتواه من مكون أصلي. لدى React خاصية خاصة مضمنة لهذا الغرض: props.children
.
تخيل أنك بحاجة إلى مكون `Card` يمكنه تغليف أي محتوى بحدود وظل ثابتين. بدلاً من إنشاء متغيرات `TextCard` و `ImageCard` و `ProfileCard` من خلال الوراثة، يمكنك إنشاء مكون `Card` عام واحد.
// Card.js - A generic container component
function Card(props) {
return (
<div className="card">
{props.children}
</div>
);
}
// App.js - Using the Card component
function App() {
return (
<div>
<Card>
<h1>Welcome!</h1>
<p>This content is inside a Card component.</p>
</Card>
<Card>
<img src="/path/to/image.jpg" alt="An example image" />
<p>This is an image card.</p>
</Card>
</div>
);
}
هنا، لا يعرف المكون Card
أو يهتم بما يحتويه. إنه يوفر ببساطة نمط التغليف. يتم تمرير المحتوى بين علامتي الفتح والإغلاق <Card>
تلقائيًا كـ props.children
. هذا مثال جميل على الفصل وإعادة الاستخدام.
التقنية 2: التخصص باستخدام الخصائص
في بعض الأحيان، يحتاج المكون إلى عدة "ثقوب" لملئها بواسطة مكونات أخرى. في حين أنه يمكنك استخدام `props.children`، إلا أن الطريقة الأكثر وضوحًا وتنظيمًا هي تمرير المكونات كخصائص عادية. غالبًا ما يسمى هذا النمط بالتخصص.
ضع في اعتبارك مكون `Modal`. يحتوي النموذج عادةً على قسم عنوان وقسم محتوى وقسم إجراءات (مع أزرار مثل "تأكيد" أو "إلغاء"). يمكننا تصميم `Modal` لقبول هذه الأقسام كخصائص.
// Modal.js - A more specialized container
function Modal(props) {
return (
<div className="modal-backdrop">
<div className="modal-content">
<div className="modal-header">{props.title}</div>
<div className="modal-body">{props.body}</div>
<div className="modal-footer">{props.actions}</div>
</div>
</div>
);
}
// App.js - Using the Modal with specific components
function App() {
const confirmationTitle = <h2>Confirm Action</h2>;
const confirmationBody = <p>Are you sure you want to proceed with this action?</p>;
const confirmationActions = (
<div>
<button>Confirm</button>
<button>Cancel</button>
</div>
);
return (
<Modal
title={confirmationTitle}
body={confirmationBody}
actions={confirmationActions}
/>
);
}
في هذا المثال، Modal
هو مكون تخطيط قابل لإعادة الاستخدام بدرجة كبيرة. نقوم بتخصيصه عن طريق تمرير عناصر JSX محددة لـ `title` و `body` و `actions`. هذا أكثر مرونة بكثير من إنشاء فئات فرعية `ConfirmationModal` و `WarningModal`. نقوم ببساطة بتركيب Modal
بمحتوى مختلف حسب الحاجة.
التقنية 3: المكونات ذات الترتيب الأعلى (HOCs)
لمشاركة منطق غير واجهة المستخدم، مثل جلب البيانات أو المصادقة أو التسجيل، لجأ مطورو React تاريخيًا إلى نمط يسمى المكونات ذات الترتيب الأعلى (HOCs). في حين أنها استبدلت إلى حد كبير بالخطافات في React الحديثة، فمن الضروري فهمها لأنها تمثل خطوة تطورية رئيسية في قصة التركيب في React ولا تزال موجودة في العديد من قواعد التعليمات البرمجية.
HOC هي دالة تأخذ مكونًا كوسيطة وتعيد مكونًا جديدًا ومحسنًا.
دعونا ننشئ HOC يسمى `withLogger` يسجل خصائص المكون كلما تم تحديثه. هذا مفيد لتصحيح الأخطاء.
// withLogger.js - The HOC
import React, { useEffect } from 'react';
function withLogger(WrappedComponent) {
// It returns a new component...
return function EnhancedComponent(props) {
useEffect(() => {
console.log('Component updated with new props:', props);
}, [props]);
// ... that renders the original component with the original props.
return <WrappedComponent {...props} />;
};
}
// MyComponent.js - A component to be enhanced
function MyComponent({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
}
// Exporting the enhanced component
export default withLogger(MyComponent);
تقوم الدالة `withLogger` بتغليف `MyComponent`، مما يمنحها إمكانات تسجيل جديدة دون تعديل التعليمات البرمجية الداخلية لـ `MyComponent`. يمكننا تطبيق نفس HOC على أي مكون آخر لمنحه نفس ميزة التسجيل.
تحديات مع HOCs:
- جحيم التغليف: يمكن أن يؤدي تطبيق HOCs متعددة على مكون واحد إلى مكونات متداخلة بعمق في React DevTools (مثل، `withAuth(withRouter(withLogger(MyComponent)))`)، مما يجعل تصحيح الأخطاء أمرًا صعبًا.
- تصادمات تسمية الخصائص: إذا قام HOC بحقن خاصية (مثل `data`) مستخدمة بالفعل بواسطة المكون المغلف، فيمكن الكتابة فوقها عن طريق الخطأ.
- المنطق الضمني: ليس من الواضح دائمًا من التعليمات البرمجية للمكون من أين تأتي خصائصه. المنطق مخفي داخل HOC.
التقنية 4: Render Props
ظهر نمط Render Prop كحل لبعض أوجه القصور في HOCs. إنه يوفر طريقة أكثر وضوحًا لمشاركة المنطق.
يأخذ المكون الذي يحتوي على render prop دالة كخاصية (تسمى عادةً `render`) ويستدعي هذه الدالة لتحديد ما سيتم عرضه، وتمرير أي حالة أو منطق كوسيطات إليها.
دعونا ننشئ مكون `MouseTracker` يتتبع إحداثيات X و Y للماوس ويجعلها متاحة لأي مكون يريد استخدامها.
// MouseTracker.js - Component with a render prop
import React, { useState, useEffect } from 'react';
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
// Call the render function with the state
return render(position);
}
// App.js - Using the MouseTracker
function App() {
return (
<div>
<h1>Move your mouse around!</h1>
<MouseTracker
render={mousePosition => (
<p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
)}
/>
</div>
);
}
هنا، يغلف `MouseTracker` كل المنطق لتتبع حركة الماوس. لا يعرض أي شيء بمفرده. بدلاً من ذلك، فإنه يفوض منطق العرض إلى خاصية `render` الخاصة به. هذا أكثر وضوحًا من HOCs لأنه يمكنك أن ترى بالضبط من أين تأتي بيانات `mousePosition` داخل JSX.
يمكن أيضًا استخدام خاصية `children` كدالة، وهي اختلاف شائع وأنيق لهذا النمط:
// Using children as a function
<MouseTracker>
{mousePosition => (
<p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
)}
</MouseTracker>
التقنية 5: الخطافات (النهج الحديث والمفضل)
أحدثت الخطافات، التي تم تقديمها في React 16.8، ثورة في الطريقة التي نكتب بها مكونات React. فهي تسمح لك باستخدام الحالة وميزات React الأخرى في المكونات الوظيفية. والأهم من ذلك، توفر الخطافات المخصصة الحل الأكثر أناقة ومباشرة لمشاركة المنطق ذي الحالة بين المكونات.
تحل الخطافات مشاكل HOCs و Render Props بطريقة أنظف بكثير. دعونا نعيد بناء مثال `MouseTracker` الخاص بنا إلى خطاف مخصص يسمى `useMousePosition`.
// hooks/useMousePosition.js - A custom Hook
import { useState, useEffect } from 'react';
export function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // Empty dependency array means this effect runs only once
return position;
}
// DisplayMousePosition.js - A component using the Hook
import { useMousePosition } from './hooks/useMousePosition';
function DisplayMousePosition() {
const position = useMousePosition(); // Just call the hook!
return (
<p>
The mouse position is ({position.x}, {position.y})
</p>
);
}
// Another component, maybe an interactive element
import { useMousePosition } from './hooks/useMousePosition';
function InteractiveBox() {
const { x, y } = useMousePosition();
const style = {
position: 'absolute',
top: y - 25, // Center the box on the cursor
left: x - 25,
width: '50px',
height: '50px',
backgroundColor: 'lightblue',
};
return <div style={style} />;
}
هذا تحسن كبير. لا يوجد "جحيم تغليف"، ولا تصادمات في تسمية الخصائص، ولا توجد دوال render prop معقدة. يتم فصل المنطق تمامًا إلى دالة قابلة لإعادة الاستخدام (`useMousePosition`)، ويمكن لأي مكون "الاتصال" بهذا المنطق ذي الحالة بسطر واحد وواضح من التعليمات البرمجية. الخطافات المخصصة هي التعبير النهائي عن التركيب في React الحديثة، مما يسمح لك ببناء مكتبتك الخاصة من كتل المنطق القابلة لإعادة الاستخدام.
مقارنة سريعة: التركيب مقابل الوراثة في React
لتلخيص الاختلافات الرئيسية في سياق React، إليك مقارنة مباشرة:
الجانب | الوراثة (نمط مضاد في React) | التركيب (المفضل في React) |
---|---|---|
العلاقة | علاقة "هو-نوع-من". المكون المتخصص هو نوع من مكون أساسي. | علاقة "يملك-نوع-من" أو "يستخدم-نوع-من". المكون المعقد يملك مكونات أصغر أو يستخدم منطقًا مشتركًا. |
الاقتران | عالي. المكونات الفرعية مقترنة بإحكام بتنفيذ الأصل. | منخفض. المكونات مستقلة ويمكن إعادة استخدامها في سياقات مختلفة دون تعديل. |
المرونة | منخفض. تجعل التسلسلات الهرمية الجامدة القائمة على الفئات من الصعب مشاركة المنطق عبر أشجار المكونات المختلفة. | عالي. يمكن دمج المنطق وواجهة المستخدم وإعادة استخدامهما بطرق لا حصر لها، مثل وحدات البناء. |
إعادة استخدام التعليمات البرمجية | محدودة بالتسلسل الهرمي المحدد مسبقًا. تحصل على "الغوريلا" بأكملها عندما تريد فقط "الموزة". | ممتاز. يمكن استخدام المكونات والخطافات الصغيرة والمركزة عبر التطبيق بأكمله. |
مصطلح React | لا يشجع عليه فريق React الرسمي. | النهج الموصى به والاصطلاحي لبناء تطبيقات React. |
الخلاصة: فكر في التركيب
تعتبر المناقشة بين التركيب والوراثة موضوعًا تأسيسيًا في تصميم البرمجيات. في حين أن الوراثة لها مكانها في OOP الكلاسيكية، إلا أن الطبيعة الديناميكية والقائمة على المكونات لتطوير واجهة المستخدم تجعلها غير مناسبة لـ React. تم تصميم المكتبة بشكل أساسي لاحتضان التركيب.
من خلال تفضيل التركيب، فإنك تكسب:
- المرونة: القدرة على مزج واجهة المستخدم والمنطق ومطابقتها حسب الحاجة.
- قابلية الصيانة: يسهل فهم المكونات المقترنة بشكل فضفاض واختبارها وإعادة بنائها بمعزل عن غيرها.
- قابلية التوسع: تشجع العقلية التركيبية على إنشاء نظام تصميم للمكونات والخطافات الصغيرة والقابلة لإعادة الاستخدام التي يمكن استخدامها لبناء تطبيقات كبيرة ومعقدة بكفاءة.
بصفتك مطور React عالميًا، فإن إتقان التركيب لا يتعلق فقط باتباع أفضل الممارسات - بل يتعلق بفهم الفلسفة الأساسية التي تجعل React أداة قوية ومنتجة. ابدأ بإنشاء مكونات صغيرة ومركزة. استخدم `props.children` للحاويات العامة والخصائص للتخصص. لمشاركة المنطق، استخدم الخطافات المخصصة أولاً. من خلال التفكير في التركيب، ستكون في طريقك لبناء تطبيقات React أنيقة وقوية وقابلة للتطوير تصمد أمام اختبار الزمن.