الفرق بين المراجعتين لصفحة: «React/higher order components»
Kinan-mawed (نقاش | مساهمات) لا ملخص تعديل |
تحديث |
||
(9 مراجعات متوسطة بواسطة 3 مستخدمين غير معروضة) | |||
سطر 1: | سطر 1: | ||
<noinclude>{{DISPLAYTITLE:المكونات ذات الترتيب الأعلى}}</noinclude> | <noinclude>{{DISPLAYTITLE:المكونات ذات الترتيب الأعلى في React}}</noinclude> | ||
إنّ المكوّنات ذات الترتيب الأعلى (Higher-Order Components | إنّ المكوّنات ذات الترتيب الأعلى (Higher-Order Components واختصارًا HOC) هي تقنية متقدمة في React لإعادة استخدام منطق المكونات. وهي ليست جزءًا من واجهة برمجة تطبيقات React API، بل هي نمط ينبثق عن طبيعة React التركيبية. | ||
باختصار، يكون المكوّن ذو الترتيب الأعلى عبارة عن دالة تأخذ مكوّنًا وتُعيد مُكوّنًا جديدًا:<syntaxhighlight lang="javascript"> | |||
const EnhancedComponent = higherOrderComponent(WrappedComponent); | const EnhancedComponent = higherOrderComponent(WrappedComponent); | ||
</syntaxhighlight>وفيما يُحوّل المكوّن الخاصيّات إلى واجهة مستخدم، يُحوّل المكوّن ذو الترتيب الأعلى مكوّنًا إلى مكوّن آخر. | </syntaxhighlight>وفيما يُحوّل المكوّن الخاصيّات إلى واجهة مستخدم، يُحوّل المكوّن ذو الترتيب الأعلى مكوّنًا إلى مكوّن آخر. | ||
تكون المكوّنات ذات الترتيب الأعلى شائعة في مكتبات React المُقدَّمة من طرف ثالث، مثل مكتبة connect الخاصة بـ Redux و مكتبة createFragmentContainer الخاصّة بـ Relay. | تكون المكوّنات ذات الترتيب الأعلى شائعة في مكتبات React المُقدَّمة من طرف ثالث، مثل مكتبة <code>[https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options connect]</code> الخاصة بـ Redux و مكتبة <code>[http://facebook.github.io/relay/docs/en/fragment-container.html createFragmentContainer]</code> الخاصّة بـ Relay. | ||
سنناقش في هذه الصفحة الفائدة من المكوّنات ذات الترتيب الأعلى وكيفية كتابتها. | سنناقش في هذه الصفحة الفائدة من المكوّنات ذات الترتيب الأعلى وكيفية كتابتها. | ||
== استخدام المكوّنات ذات الترتيب الأعلى لأجل الاهتمامات المشتركة == | == استخدام المكوّنات ذات الترتيب الأعلى لأجل الاهتمامات المشتركة == | ||
'''ملاحظة:''' أشرنا سابقًا إلى أفضلية استخدام المخاليط (mixins) كطريقة للتعامل مع الاهتمامات المشتركة (cross-cutting concerns)، ولكننا أدركنا بعد ذلك أنّ المخاليط تُسبّب مشاكل أكثر من فائدتها. تعرّف من هنا عن سبب انتقالنا من المخاليط وكيفية تحويل مكوّناتك الحالية التي تستخدمها. | '''ملاحظة:''' أشرنا سابقًا إلى أفضلية استخدام المخاليط (mixins) كطريقة للتعامل مع الاهتمامات المشتركة (cross-cutting concerns)، ولكننا أدركنا بعد ذلك أنّ المخاليط تُسبّب مشاكل أكثر من فائدتها. تعرّف [https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html من هنا] عن سبب انتقالنا من المخاليط وكيفية تحويل مكوّناتك الحالية التي تستخدمها. | ||
تُشكِّل المكوّنات الوحدة الأساسية لإعادة استخدام الشيفرة في React، ولكنّك ستجد بعض الأنماط التي لا تتلاءم بشكل مباشر مع المكوّنات التقليدية. | تُشكِّل المكوّنات الوحدة الأساسية لإعادة استخدام الشيفرة في React، ولكنّك ستجد بعض الأنماط التي لا تتلاءم بشكل مباشر مع المكوّنات التقليدية. | ||
سطر 38: | سطر 38: | ||
handleChange() { | handleChange() { | ||
// تحديث حالة المكون عند تغيير مصدر البيانات | // تحديث حالة المكون عند تغيير مصدر البيانات | ||
this.setState({ | this.setState({ | ||
comments: DataSource.getComments() | comments: DataSource.getComments() | ||
سطر 53: | سطر 54: | ||
} | } | ||
} | } | ||
</syntaxhighlight>ولاحقًا قررت كتابة مكوّن للاشتراك بمنشور وحيد في المدوّنة، والذي يتبع نفس النمط:<syntaxhighlight lang="javascript"> | </syntaxhighlight>ولاحقًا قررت كتابة مكوّن للاشتراك بمنشور وحيد في المدوّنة، والذي يتبع نفس النمط:<syntaxhighlight lang="javascript"> | ||
سطر 82: | سطر 98: | ||
} | } | ||
} | } | ||
</syntaxhighlight>إنّ المكوّنين <code>CommentList</code> و <code>BlogPost</code> غير متطابقان، فهما يستدعيان توابع مختلفة على مصدر البيانات <code>DataSource</code>، ويُصيّران ناتجًا مختلفًا، ولكن يتشابه تنفيذهما الداخلي كثيرًا في ما يلي: | </syntaxhighlight>إنّ المكوّنين <code>CommentList</code> و <code>BlogPost</code> غير متطابقان، فهما يستدعيان توابع مختلفة على مصدر البيانات <code>DataSource</code>، ويُصيّران ناتجًا مختلفًا، ولكن يتشابه تنفيذهما الداخلي كثيرًا في ما يلي: | ||
* إضافة مُستمِع (listener) للتغيير إلى <code>DataSource</code> عند الوصل (mount). | * إضافة مُستمِع (listener) للتغيير إلى <code>DataSource</code> عند الوصل (mount). | ||
سطر 140: | سطر 155: | ||
} | } | ||
</syntaxhighlight>لاحظ أنّ المكوّن | </syntaxhighlight>لاحظ أنّ المكوّن ذي الترتيب الأعلى لا يُعدِّل مكوّن حقل الإدخال ولا يستخدم الوراثة لنسخ سلوكه، بل يُركِّب المكوّن الأساسي عن طريق تغليفه في مكوّن حاوية. المكوّن ذو الترتيب الأعلى هو عبارة عن دالة نقيّة (pure) بدون أي تأُثيرات جانبية إطلاقًا. | ||
يستقبل المكوّن المُغلَّف جميع الخاصيّات من الحاوية بالإضافة إلى الخاصيّة الجديدة وهي <code>data</code> والتي يستخدمها لتصيير ناتجه. لا يهتم المكوّن | يستقبل المكوّن المُغلَّف جميع الخاصيّات من الحاوية بالإضافة إلى الخاصيّة الجديدة وهي <code>data</code> والتي يستخدمها لتصيير ناتجه. لا يهتم المكوّن ذو الترتيب الأعلى بكيفية أو سبب استخدام البيانات، ولا يهتم المكوّن المُغلَّف بمصدر البيانات. | ||
بما أنّ <code>withSubscription</code> هو دالة عادية بإمكانك إضافة وسائط لها كما تريد. فقد ترغب مثلًا بجعل اسم الخاصيّة <code>data</code> قابلًا للإعداد، وذلك لعزل المكوّن | بما أنّ <code>withSubscription</code> هو دالة عادية بإمكانك إضافة وسائط لها كما تريد. فقد ترغب مثلًا بجعل اسم الخاصيّة <code>data</code> قابلًا للإعداد، وذلك لعزل المكوّن ذي الترتيب الأعلى عن المكوّن المُغلِّف له، أو تستطيع قبول وسيط يُعِد <code>shouldComponentUpdate</code> أو مصدر البيانات. كل هذه الإمكانيات متوفرة بسبب امتلاك المكوّن ذو الترتيب الأعلى السيطرة على كيفيّة تعريف المكوّنات. | ||
وكما هو الحال مع المكوّنات يكون العقد بين <code>withSubscription</code> والمكوّن المغلّف معتمد بشكل كامل على الخاصيّات. يجعل هذا من السهل استبدال مكوّن | وكما هو الحال مع المكوّنات يكون العقد بين <code>withSubscription</code> والمكوّن المغلّف معتمد بشكل كامل على الخاصيّات. يجعل هذا من السهل استبدال مكوّن ذو ترتيب أعلى بواحد آخر، طالما أنّهما يعطيان نفس الخاصيات للمكوّن المغلّف. قد يكون هذا مفيدًا إن غيرت مكتبة الحصول على البيانات مثلًا. | ||
== لا تُعدِّل المكوّن الأصلي بل استخدم التراكيب == | == لا تُعدِّل المكوّن الأصلي بل استخدم التراكيب == | ||
قاوم رغبة تعديل نموذج المكوّن بداخل المكوّن | قاوم رغبة تعديل نموذج المكوّن بداخل المكوّن ذو الترتيب الأعلى:<syntaxhighlight lang="javascript"> | ||
function logProps(InputComponent) { | function logProps(InputComponent) { | ||
InputComponent.prototype. | InputComponent.prototype.componentDidUpdate = function(prevProps) { | ||
console.log('Current props: ', this.props); | console.log('Current props: ', this.props); | ||
console.log(' | console.log('Previous props: ', prevProps); | ||
}; | }; | ||
// حقيقة أننا نعيد حقل الإدخال الأصلي هي تلميح إلى أنّه قد تغيّر | // حقيقة أننا نعيد حقل الإدخال الأصلي هي تلميح إلى أنّه قد تغيّر | ||
return InputComponent; | return InputComponent; | ||
سطر 162: | سطر 178: | ||
const EnhancedComponent = logProps(InputComponent); | const EnhancedComponent = logProps(InputComponent); | ||
</syntaxhighlight>هنالك بعض المشاكل عند فعل ذلك. أحدها هي عدم القدرة على استخدام مكوّن حقل الإدخال بشكل منفصل عن المكوّن <code>EnhancedComponent</code>. وإن طبقت مكوّن | </syntaxhighlight>هنالك بعض المشاكل عند فعل ذلك. أحدها هي عدم القدرة على استخدام مكوّن حقل الإدخال بشكل منفصل عن المكوّن <code>EnhancedComponent</code>. وإن طبقت مكوّن ذو ترتيب أعلى آخر إلى المكوّن <code>EnhancedComponent</code> والذي يُعدِّل أيضًا <code>componentWillReceiveProps</code>، فسيتجاوز وظيفة المكوّن ذو الترتيب الأعلى الأول! لا يعمل المكوّن ذو الترتيب الأعلى هذا أيضًا مع المكوّنات الدالية لأنّها لا تمتلك توابع دورة الحياة. | ||
إنّ تعديل المكوّنات | إنّ تعديل المكوّنات ذات الترتيب الأعلى ليس أمرًا بسيطًا فيجب معرفة كيفية تنفيذها من أجل تجنب التعارض مع المكوّنات ذات الترتيب الأعلى الأخرى. | ||
بدلًا من تعديل المكوّن | بدلًا من تعديل المكوّن ذو الترتيب الأعلى يجب استخدام التراكيب عن طريق تغليف مكوّن حقل الإدخال في مكوّن حاوية:<syntaxhighlight lang="javascript"> | ||
function logProps(WrappedComponent) { | function logProps(WrappedComponent) { | ||
return class extends React.Component { | return class extends React.Component { | ||
componentDidUpdate(prevProps) { | |||
console.log('Current props: ', this.props); | console.log('Current props: ', this.props); | ||
console.log(' | console.log('Previous props: ', prevProps); | ||
} | } | ||
render() { | render() { | ||
سطر 180: | سطر 196: | ||
} | } | ||
</syntaxhighlight>يمتلك هذا المكوّن | </syntaxhighlight>يمتلك هذا المكوّن ذو الترتيب الأعلى نفس وظيفة نسخة التعديل مع تجنب الأخطاء المحتملة، ويعمل بشكل متكافئ مع مكوّنات الأصناف والدوال. وبما أنّه دالة نقيّة فهو قابل للتركيب مع مكوّنات ذات الترتيب الأعلى الأخرى أو حتى مع نفسه. | ||
ربما قد لاحظت التشابه بين المكوّنات | ربما قد لاحظت التشابه بين المكوّنات ذات الترتيب الأعلى وبين النمط الذي يُدعى المكوّنات الحاوية (container components) والتي هي جزء من استراتيجية فصل المسؤولية بين الاهتمامات ذات المستوى الأعلى والاهتمامات ذات المستوى الأدنى. تُدير الحاويات أشياء مثل الاشتراكات والحالة وتُمرِّر خاصيّات للمكوّنات والتي تتعامل مع أشياء مثل تصيير واجهة المستخدم. تستخدم المكوّنات ذات الترتيب الأعلى الحاويات كجزء منها. بإمكانك النظر إلى المكوّنات ذات الترتيب الأعلى كتعاريف للمكوّنات الحاوية. | ||
== تمرير الخاصيات غير المرتبطة إلى المكون المغلف == | == تمرير الخاصيات غير المرتبطة إلى المكون المغلف == | ||
تُضيف المكوّنات | تُضيف المكوّنات ذات الترتيب الأعلى ميزات إلى المكوّن. ولكنها لا يجب عليها تغييره. من المتوقع أن يمتلك المكوّن العائد من المكوّن ذو الترتيب الأعلى نفس الواجهة للمكوّن المغلف له. | ||
يجب على المكوّنات | يجب على المكوّنات ذات الترتيب الأعلى تمرير الخاصيّات غير المرتبطة بأي اهتمام محدّد. تحتوي معظم المكوّنات ذات الترتيب الأعلى على تابع للتصيير والذي يبدو مشابهًا لما يلي:<syntaxhighlight lang="javascript"> | ||
render() { | render() { | ||
// ترشيح خاصيات إضافية مخصصة لهذا المكون عالي الترتيب والتي لا يجب تمريرها | // ترشيح خاصيات إضافية مخصصة لهذا المكون عالي الترتيب والتي لا يجب تمريرها | ||
سطر 206: | سطر 222: | ||
} | } | ||
</syntaxhighlight>يضمن هذا أن تكون المكوّنات | </syntaxhighlight>يضمن هذا أن تكون المكوّنات ذات الترتيب الأعلى مرنة وقابلة لإعادة الاستخدام قدر الإمكان. | ||
== رفع إمكانية التركيب إلى أقصى درجة == | == رفع إمكانية التركيب إلى أقصى درجة == | ||
لا تبدو كافة المكونات | لا تبدو كافة المكونات ذات الترتيب الأعلى مثل بعضها. فأحيانًا تقبل فقط وسيط واحد، وهو المكون المغلف:<syntaxhighlight lang="javascript"> | ||
const NavbarWithRouter = withRouter(Navbar); | |||
</syntaxhighlight>تقبل المكوّنات ذات الترتيب الأعلى وسائط إضافية عادةً. في هذا المثال نستخدم كائن للإعدادات لتحديد اعتماديات بيانات المكوّن:<syntaxhighlight lang="javascript"> | |||
const CommentWithRelay = Relay.createContainer(Comment, config); | |||
</syntaxhighlight>يبدو أشيع شكل للمكوّنات ذات الترتيب الأعلى كما يلي:<syntaxhighlight lang="javascript"> | |||
// React Redux's `connect` | |||
const ConnectedComment = connect(commentSelector, commentActions)(CommentList); | |||
</syntaxhighlight>إن قسّمته إلى أقسام أصغر فمن الأسهل عليك فهم ما يحدث:<syntaxhighlight lang="javascript"> | |||
// connect هو دالة تعيد دالة أخرى | |||
const enhance = connect(commentListSelector, commentListActions); | |||
// الدالة المعادة هي كائن عالي الترتيب والذي يعيد كائن متصل مع مخزن Redux | |||
const ConnectedComment = enhance(CommentList); | |||
</syntaxhighlight>وبكلمات أخرى <code>connect</code> هو عبارة عن دالة ذات ترتيب أعلى تُعيد مكوّن ذو ترتيب أعلى. | |||
قد يبدو هذا الشكل مربكًا وغير ضروري، ولكنّه يمتلك خاصيّة مفيدة. تمتلك المكونات ذات الترتيب الأعلى ذات الوسيط الوحيد مثل الذي أعادته الدالة <code>connect</code> الشكل <code>Component => Component</code>. من السهل تركيب الدوال التي نوع خرجها مطابق لنوع دخلها معًا:<syntaxhighlight lang="javascript"> | |||
// بدلًا من فعل هذا | |||
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) | |||
// تستطيع اسختدام أداة تركيب الدالة | |||
// compose(f, g, h) هي نفسها (...args) => f(g(h(...args))) | |||
const enhance = compose( | |||
// هذه مكونات عالية الترتيب مع وسيط واحد | |||
withRouter, | |||
connect(commentSelector) | |||
) | |||
const EnhancedComponent = enhance(WrappedComponent) | |||
</syntaxhighlight>(تسمح نفس هذه الخاصية للدالة <code>connect</code> باستخدام المنسقات <code>decorators</code> وهي اقتراح لا يزال تجريبيًّا في JavaScript). | |||
تتوفر الدالة <code>compose</code> عن طريق مكتبات طرف ثالث عديدة بما في ذلك lodash (مثل <code>[https://lodash.com/docs/#flowRight lodash.flowRight]</code>)، و [http://redux.js.org/docs/api/compose.html Redux]، و [http://ramdajs.com/docs/#compose Ramda]. | |||
== تغليف الاسم المعروض لسهولة تنقيح الأخطاء == | |||
تظهر مكوّنات الحاوية التي تُنشئها المكوّنات ذات الترتيب الأعلى في [https://github.com/facebook/react-devtools أدوات تطوير React] كأي مكوّنات أخرى. ولسهولة تنقيح الأخطاء اختر الاسم المعروض بحيث يتواصل وكأنه نتيجة للمكوّن ذو الترتيب الأعلى. | |||
أشيع طريقة هي تغليف الاسم المعروض للمكوّن المُغلَّف. لذا إن كان اسم المكوّن ذو الترتيب الأعلى هو <code>withSubscription</code> والاسم المعروض للمكوّن المُغلَّف هو <code>CommentList</code> فاستخدم الاسم المعروض <code>WithSubscription(CommentList)</code>:<syntaxhighlight lang="javascript"> | |||
function withSubscription(WrappedComponent) { | |||
class WithSubscription extends React.Component {/* ... */} | |||
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; | |||
return WithSubscription; | |||
} | |||
function getDisplayName(WrappedComponent) { | |||
return WrappedComponent.displayName || WrappedComponent.name || 'Component'; | |||
} | |||
</syntaxhighlight> | |||
== محاذير == | |||
تأتي المكوّنات ذو الترتيب الأعلى مع بعض المحاذير التي قد لا تكون واضحة مباشرةً إن كنت جديدًا على React. | |||
=== لا تستخدم المكوّنات ذات الترتيب الأعلى بداخل تابع التصيير === | |||
تستخدم خوارزمية المقارنة في React (وتُدعى reconciliation أي المطابقة) هوية المكوّن لتحديد إذا ما كان يجب عليها تحديث الشجرة الفرعية الحالية أو رميها ووصل واحدة جديدة. إن كان المكوّن العائد من التابع <code>render</code> مُطابِقًا تمامًا (<code>===</code>) للمكوّن من التصيير السابق، فستُحدِّث React الشجرة الفرعية عن طريق مقارنتها مع الجديدة، إن لم تكونا متطابقتين فستفصل الشجرة الفرعية السابقة بشكل كامل. | |||
لا تحتاج عادةً إلى التفكير في هذا، ولكنّه يهم في المكوّنات ذات الترتيب الأعلى لأنه يعني أنّك لا تستطيع تطبيقها على مكوّن بداخل تابع التصيير لمكوّن ما:<syntaxhighlight lang="javascript"> | |||
render() { | |||
// يُنشَأ إصدار جديد من EnhancedComponent عند كل تصيير | |||
// EnhancedComponent1 !== EnhancedComponent2 | |||
const EnhancedComponent = enhance(MyComponent); | |||
// يسبب ذلك فصل وإعادة وصل كامل الشجرة الفرعية في كل مرة | |||
return <EnhancedComponent />; | |||
} | |||
</syntaxhighlight>لا تتعلق المشكلة هنا فقط بالأداء، فإعادة وصل المكوّن تؤدّي لخسارة حالته وكافة مكوّناته الأبناء. | |||
طبق المكوّنات ذات الترتيب الأعلى بدلًا من ذلك خارج تعريف المكوّن بحيث ينشأ المكوّن الناتج مرة واحدة فقط، بعدها ستكون هويته ثابتة عبر التصييرات. وهذا هو ما تريده عادةً على أيّة حال. | |||
في تلك الحالات النادرة التي تحتاج فيها إلى تطبيق المكوّنات ذات الترتيب الأعلى بشكل ديناميكي فبإمكانك أيضًا فعل ذلك بداخل توابع دورة حياة المكوّن أو دالته البانية. | |||
=== يجب نسخ التوابع الثابتة === | |||
من المفيد أحيانًا تعريف تابع ثابت (static) في مكوّن React. فمثلًا تعرض الحاويات تابعًا ثابتًا يُدعى <code>getFragment</code> لتسهيل تركيب أجزاء GraphQL. | |||
عند تطبيق المكوّنات ذات الترتيب الأعلى على المكوّن، فسيُغلَّف المكوّن بمكوّن حاوي له. يعني هذا عدم امتلاك المكوّن الجديد لأي من التوابع الثابتة للمكوّن الأصلي:<syntaxhighlight lang="javascript"> | |||
// تعريف تابع ثابت | |||
WrappedComponent.staticMethod = function() {/*...*/} | |||
// طبق الآن المكون ذو الترتيب الأعلى | |||
const EnhancedComponent = enhance(WrappedComponent); | |||
// يجب ألا يمتلك المكون EnhancedComponent أي تابع ثابت | |||
typeof EnhancedComponent.staticMethod === 'undefined' // true | |||
</syntaxhighlight>لحل هذه المشكلة بإمكانك نسخ التوابع إلى الحاوية قبل إعادتها:<syntaxhighlight lang="javascript"> | |||
function enhance(WrappedComponent) { | |||
class Enhance extends React.Component {/*...*/} | |||
// يجب أن تعلم تمامًا أي توابع يجب نسخها | |||
Enhance.staticMethod = WrappedComponent.staticMethod; | |||
return Enhance; | |||
} | |||
</syntaxhighlight>ولكن يتطلب هذا معرفة أي توابع تحتاج إلى نسخها. بإمكانك استخدام [https://github.com/mridgway/hoist-non-react-statics هذه الإضافة] لنسخ جميع التوابع الثابتة غير المتعلقة بمكتبة React:<syntaxhighlight lang="javascript"> | |||
import hoistNonReactStatic from 'hoist-non-react-statics'; | |||
function enhance(WrappedComponent) { | |||
class Enhance extends React.Component {/*...*/} | |||
hoistNonReactStatic(Enhance, WrappedComponent); | |||
return Enhance; | |||
} | |||
</syntaxhighlight>من الحلول الممكنة الأخرى هي استخراج التابع الثابت بشكل منفصل من المكوّن نفسه:<syntaxhighlight lang="javascript"> | |||
// بدلًا من كتابة ما يلي | |||
MyComponent.someFunction = someFunction; | |||
export default MyComponent; | |||
// استخرج التابع بشكل منفصل | |||
export { someFunction }; | |||
// وفي الوحدة المستهلك استورد كليهما معًا | |||
import MyComponent, { someFunction } from './MyComponent.js'; | |||
</syntaxhighlight> | |||
=== لا تستطيع تمرير المراجع === | |||
بينما يكون الغرض من استخدام المكوّنات ذات الترتيب الأعلى هو تمرير كافة الخاصيّات للكائن المُغلَّف فلا يعمل هذا بالنسبة للمراجع. وهذا بسبب عدم كونها خاصيّة مثل المفتاح key. حيث تتعامل معها React بشكلٍ خاص. إن أضفت مرجع ref إلى عنصر مكوّنه ناتج عن مكوّن ذو ترتيب أعلى، فسيشير المرجع إلى نسخة عن المكوّن الحاوي وليس المكوّن المُغلَّف. | |||
حل هذه المشكلة هو استخدام <code>React.forwardRef</code> (المقدمة في إصدار React 16.3). تعلم المزيد حولها في قسم [[React/forwarding refs|تمرير المراجع]]. | |||
== انظر أيضًا == | |||
* [[React/jsx in depth|شرح JSX بالتفصيل]] | |||
* [[React/static type checking|التحقق من الأنواع الثابتة]] | |||
* [[React/typechecking with proptypes|التحقق من الأنواع باستخدام PropTypes]] | |||
* [[React/refs and the dom|استخدام المراجع مع DOM]] | |||
* [[React/uncontrolled components|المكونات غير المضبوطة]] | |||
* [[React/optimizing performance|تحسين الأداء]] | |||
* [[React/react without es6|React بدون ES6]] | |||
* [[React/react without jsx|React بدون JSX]] | |||
* [[React/reconciliation|المطابقة (Reconciliation)]] | |||
* [[React/context|استخدام السياق (Context) في React]] | |||
* [[React/fragments|استخدام الأجزاء (Fragments) في React]] | |||
* [[React/portals|المداخل (Portals) في React]] | |||
* [[React/error boundaries|حدود الأخطاء]] | |||
* [[React/web components|مكونات الويب]] | |||
* [[React/forwarding refs|تمرير المراجع]] | |||
* [[React/render props|خاصيات التصيير]] | |||
* [[React/integrating with other libraries|تكامل React مع المكتبات الأخرى]] | |||
* [[React/accessibility|سهولة الوصول]] | |||
* [[React/code splitting|تقسيم الشيفرة]] | |||
* [[React/strict mode|الوضع الصارم (Strict Mode)]] | |||
== مصادر== | |||
*[https://reactjs.org/docs/higher-order-components.html صفحة المكونات ذات الترتيب الأعلى في توثيق React الرسمي]. | |||
[[تصنيف:React]] | |||
[[تصنيف:React Advanced Guides]] |
المراجعة الحالية بتاريخ 18:37، 3 نوفمبر 2020
إنّ المكوّنات ذات الترتيب الأعلى (Higher-Order Components واختصارًا HOC) هي تقنية متقدمة في React لإعادة استخدام منطق المكونات. وهي ليست جزءًا من واجهة برمجة تطبيقات React API، بل هي نمط ينبثق عن طبيعة React التركيبية.
باختصار، يكون المكوّن ذو الترتيب الأعلى عبارة عن دالة تأخذ مكوّنًا وتُعيد مُكوّنًا جديدًا:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
وفيما يُحوّل المكوّن الخاصيّات إلى واجهة مستخدم، يُحوّل المكوّن ذو الترتيب الأعلى مكوّنًا إلى مكوّن آخر.
تكون المكوّنات ذات الترتيب الأعلى شائعة في مكتبات React المُقدَّمة من طرف ثالث، مثل مكتبة connect
الخاصة بـ Redux و مكتبة createFragmentContainer
الخاصّة بـ Relay.
سنناقش في هذه الصفحة الفائدة من المكوّنات ذات الترتيب الأعلى وكيفية كتابتها.
استخدام المكوّنات ذات الترتيب الأعلى لأجل الاهتمامات المشتركة
ملاحظة: أشرنا سابقًا إلى أفضلية استخدام المخاليط (mixins) كطريقة للتعامل مع الاهتمامات المشتركة (cross-cutting concerns)، ولكننا أدركنا بعد ذلك أنّ المخاليط تُسبّب مشاكل أكثر من فائدتها. تعرّف من هنا عن سبب انتقالنا من المخاليط وكيفية تحويل مكوّناتك الحالية التي تستخدمها.
تُشكِّل المكوّنات الوحدة الأساسية لإعادة استخدام الشيفرة في React، ولكنّك ستجد بعض الأنماط التي لا تتلاءم بشكل مباشر مع المكوّنات التقليدية.
افترض مثلًا أنّه لديك مكوّن لقائمة التعليقات يُدعى CommentList
والذي يشترك بمصدر بيانات خارجي لتصيير قائمة من التعليقات:
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" هو مصدر بيانات عام
comments: DataSource.getComments()
};
}
componentDidMount() {
// الاشتراك بالتغييرات
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// مسح المستمع listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// تحديث حالة المكون عند تغيير مصدر البيانات
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
ولاحقًا قررت كتابة مكوّن للاشتراك بمنشور وحيد في المدوّنة، والذي يتبع نفس النمط:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
إنّ المكوّنين CommentList
و BlogPost
غير متطابقان، فهما يستدعيان توابع مختلفة على مصدر البيانات DataSource
، ويُصيّران ناتجًا مختلفًا، ولكن يتشابه تنفيذهما الداخلي كثيرًا في ما يلي:
- إضافة مُستمِع (listener) للتغيير إلى
DataSource
عند الوصل (mount). - استدعاء
setState
بداخل المُستمِع عند تغيّر مصدر البيانات. - إزالة مُستمِع التغيير عند الفصل (unmount).
بإمكانك أن تتخيّل في التطبيقات الكبيرة تكرار هذا النمط من الاشتراك بمصدر البيانات DataSource
واستدعاء setState
. نريد وحدة مُجرَّدة تسمح لنا بتعريف هذا المنطق في مكان واحد ومشاركته عبر مكوّنات عديدة. وهنا تأتي فائدة المكوّنات ذات الترتيب الأعلى.
نستطيع كتابة دالة تُنشِئ مكوّنات، مثل CommentList
و BlogPost
والتي تشترك بمصدر البيانات DataSource
. تقبل هذه الدالة كوسيط لها المكوّن الابن الذي يستقبل البيانات المُشارَكَة كخاصيّة له. فلنسمّي هذه الدالة withSubscription
:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
المُعامِل الأول هو المكوّن المُغلَّف. يسترجع المُعامِل الثاني البيانات التي تُهمّنا، مع إعطاء مصدر البيانات DataSource
والخاصيّات الحاليّة.
عند تصيير CommentListWithSubscription
و BlogPostWithSubscription
، فسيُمرِّر المكوّنان CommentList
و BlogPost
خاصيّة للبيانات data
والتي تحمل أحدث البيانات المستخرجة من DataSource
:
// تأخذ هذه الدالة مكوّن
function withSubscription(WrappedComponent, selectData) {
// وتعيد مكون آخر
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// والذي يهتم بالاشتراك
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ويصير المكون المغلف مع البيانات الجديدة
// لاحظ أننا مررنا أي خاصيات إضافية
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
لاحظ أنّ المكوّن ذي الترتيب الأعلى لا يُعدِّل مكوّن حقل الإدخال ولا يستخدم الوراثة لنسخ سلوكه، بل يُركِّب المكوّن الأساسي عن طريق تغليفه في مكوّن حاوية. المكوّن ذو الترتيب الأعلى هو عبارة عن دالة نقيّة (pure) بدون أي تأُثيرات جانبية إطلاقًا.
يستقبل المكوّن المُغلَّف جميع الخاصيّات من الحاوية بالإضافة إلى الخاصيّة الجديدة وهي data
والتي يستخدمها لتصيير ناتجه. لا يهتم المكوّن ذو الترتيب الأعلى بكيفية أو سبب استخدام البيانات، ولا يهتم المكوّن المُغلَّف بمصدر البيانات.
بما أنّ withSubscription
هو دالة عادية بإمكانك إضافة وسائط لها كما تريد. فقد ترغب مثلًا بجعل اسم الخاصيّة data
قابلًا للإعداد، وذلك لعزل المكوّن ذي الترتيب الأعلى عن المكوّن المُغلِّف له، أو تستطيع قبول وسيط يُعِد shouldComponentUpdate
أو مصدر البيانات. كل هذه الإمكانيات متوفرة بسبب امتلاك المكوّن ذو الترتيب الأعلى السيطرة على كيفيّة تعريف المكوّنات.
وكما هو الحال مع المكوّنات يكون العقد بين withSubscription
والمكوّن المغلّف معتمد بشكل كامل على الخاصيّات. يجعل هذا من السهل استبدال مكوّن ذو ترتيب أعلى بواحد آخر، طالما أنّهما يعطيان نفس الخاصيات للمكوّن المغلّف. قد يكون هذا مفيدًا إن غيرت مكتبة الحصول على البيانات مثلًا.
لا تُعدِّل المكوّن الأصلي بل استخدم التراكيب
قاوم رغبة تعديل نموذج المكوّن بداخل المكوّن ذو الترتيب الأعلى:
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
};
// حقيقة أننا نعيد حقل الإدخال الأصلي هي تلميح إلى أنّه قد تغيّر
return InputComponent;
}
// سيسجل المكون EnhancedComponent عندما تستقبل الخاصيّات
const EnhancedComponent = logProps(InputComponent);
هنالك بعض المشاكل عند فعل ذلك. أحدها هي عدم القدرة على استخدام مكوّن حقل الإدخال بشكل منفصل عن المكوّن EnhancedComponent
. وإن طبقت مكوّن ذو ترتيب أعلى آخر إلى المكوّن EnhancedComponent
والذي يُعدِّل أيضًا componentWillReceiveProps
، فسيتجاوز وظيفة المكوّن ذو الترتيب الأعلى الأول! لا يعمل المكوّن ذو الترتيب الأعلى هذا أيضًا مع المكوّنات الدالية لأنّها لا تمتلك توابع دورة الحياة.
إنّ تعديل المكوّنات ذات الترتيب الأعلى ليس أمرًا بسيطًا فيجب معرفة كيفية تنفيذها من أجل تجنب التعارض مع المكوّنات ذات الترتيب الأعلى الأخرى.
بدلًا من تعديل المكوّن ذو الترتيب الأعلى يجب استخدام التراكيب عن طريق تغليف مكوّن حقل الإدخال في مكوّن حاوية:
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// تغليف مكوّن حقل الإدخال في حاوية بدون تعديله
return <WrappedComponent {...this.props} />;
}
}
}
يمتلك هذا المكوّن ذو الترتيب الأعلى نفس وظيفة نسخة التعديل مع تجنب الأخطاء المحتملة، ويعمل بشكل متكافئ مع مكوّنات الأصناف والدوال. وبما أنّه دالة نقيّة فهو قابل للتركيب مع مكوّنات ذات الترتيب الأعلى الأخرى أو حتى مع نفسه.
ربما قد لاحظت التشابه بين المكوّنات ذات الترتيب الأعلى وبين النمط الذي يُدعى المكوّنات الحاوية (container components) والتي هي جزء من استراتيجية فصل المسؤولية بين الاهتمامات ذات المستوى الأعلى والاهتمامات ذات المستوى الأدنى. تُدير الحاويات أشياء مثل الاشتراكات والحالة وتُمرِّر خاصيّات للمكوّنات والتي تتعامل مع أشياء مثل تصيير واجهة المستخدم. تستخدم المكوّنات ذات الترتيب الأعلى الحاويات كجزء منها. بإمكانك النظر إلى المكوّنات ذات الترتيب الأعلى كتعاريف للمكوّنات الحاوية.
تمرير الخاصيات غير المرتبطة إلى المكون المغلف
تُضيف المكوّنات ذات الترتيب الأعلى ميزات إلى المكوّن. ولكنها لا يجب عليها تغييره. من المتوقع أن يمتلك المكوّن العائد من المكوّن ذو الترتيب الأعلى نفس الواجهة للمكوّن المغلف له.
يجب على المكوّنات ذات الترتيب الأعلى تمرير الخاصيّات غير المرتبطة بأي اهتمام محدّد. تحتوي معظم المكوّنات ذات الترتيب الأعلى على تابع للتصيير والذي يبدو مشابهًا لما يلي:
render() {
// ترشيح خاصيات إضافية مخصصة لهذا المكون عالي الترتيب والتي لا يجب تمريرها
const { extraProp, ...passThroughProps } = this.props;
// حقن الخاصيات في المكون المغلف. وهي عادة قيم الحالة أو نسخ من التوابع
const injectedProp = someStateOrInstanceMethod;
// تمرير الخاصيات إلى المكون المغلف
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
يضمن هذا أن تكون المكوّنات ذات الترتيب الأعلى مرنة وقابلة لإعادة الاستخدام قدر الإمكان.
رفع إمكانية التركيب إلى أقصى درجة
لا تبدو كافة المكونات ذات الترتيب الأعلى مثل بعضها. فأحيانًا تقبل فقط وسيط واحد، وهو المكون المغلف:
const NavbarWithRouter = withRouter(Navbar);
تقبل المكوّنات ذات الترتيب الأعلى وسائط إضافية عادةً. في هذا المثال نستخدم كائن للإعدادات لتحديد اعتماديات بيانات المكوّن:
const CommentWithRelay = Relay.createContainer(Comment, config);
يبدو أشيع شكل للمكوّنات ذات الترتيب الأعلى كما يلي:
// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
إن قسّمته إلى أقسام أصغر فمن الأسهل عليك فهم ما يحدث:
// connect هو دالة تعيد دالة أخرى
const enhance = connect(commentListSelector, commentListActions);
// الدالة المعادة هي كائن عالي الترتيب والذي يعيد كائن متصل مع مخزن Redux
const ConnectedComment = enhance(CommentList);
وبكلمات أخرى connect
هو عبارة عن دالة ذات ترتيب أعلى تُعيد مكوّن ذو ترتيب أعلى.
قد يبدو هذا الشكل مربكًا وغير ضروري، ولكنّه يمتلك خاصيّة مفيدة. تمتلك المكونات ذات الترتيب الأعلى ذات الوسيط الوحيد مثل الذي أعادته الدالة connect
الشكل Component => Component
. من السهل تركيب الدوال التي نوع خرجها مطابق لنوع دخلها معًا:
// بدلًا من فعل هذا
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// تستطيع اسختدام أداة تركيب الدالة
// compose(f, g, h) هي نفسها (...args) => f(g(h(...args)))
const enhance = compose(
// هذه مكونات عالية الترتيب مع وسيط واحد
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
(تسمح نفس هذه الخاصية للدالة connect
باستخدام المنسقات decorators
وهي اقتراح لا يزال تجريبيًّا في JavaScript).
تتوفر الدالة compose
عن طريق مكتبات طرف ثالث عديدة بما في ذلك lodash (مثل lodash.flowRight
)، و Redux، و Ramda.
تغليف الاسم المعروض لسهولة تنقيح الأخطاء
تظهر مكوّنات الحاوية التي تُنشئها المكوّنات ذات الترتيب الأعلى في أدوات تطوير React كأي مكوّنات أخرى. ولسهولة تنقيح الأخطاء اختر الاسم المعروض بحيث يتواصل وكأنه نتيجة للمكوّن ذو الترتيب الأعلى.
أشيع طريقة هي تغليف الاسم المعروض للمكوّن المُغلَّف. لذا إن كان اسم المكوّن ذو الترتيب الأعلى هو withSubscription
والاسم المعروض للمكوّن المُغلَّف هو CommentList
فاستخدم الاسم المعروض WithSubscription(CommentList)
:
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
محاذير
تأتي المكوّنات ذو الترتيب الأعلى مع بعض المحاذير التي قد لا تكون واضحة مباشرةً إن كنت جديدًا على React.
لا تستخدم المكوّنات ذات الترتيب الأعلى بداخل تابع التصيير
تستخدم خوارزمية المقارنة في React (وتُدعى reconciliation أي المطابقة) هوية المكوّن لتحديد إذا ما كان يجب عليها تحديث الشجرة الفرعية الحالية أو رميها ووصل واحدة جديدة. إن كان المكوّن العائد من التابع render
مُطابِقًا تمامًا (===
) للمكوّن من التصيير السابق، فستُحدِّث React الشجرة الفرعية عن طريق مقارنتها مع الجديدة، إن لم تكونا متطابقتين فستفصل الشجرة الفرعية السابقة بشكل كامل.
لا تحتاج عادةً إلى التفكير في هذا، ولكنّه يهم في المكوّنات ذات الترتيب الأعلى لأنه يعني أنّك لا تستطيع تطبيقها على مكوّن بداخل تابع التصيير لمكوّن ما:
render() {
// يُنشَأ إصدار جديد من EnhancedComponent عند كل تصيير
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// يسبب ذلك فصل وإعادة وصل كامل الشجرة الفرعية في كل مرة
return <EnhancedComponent />;
}
لا تتعلق المشكلة هنا فقط بالأداء، فإعادة وصل المكوّن تؤدّي لخسارة حالته وكافة مكوّناته الأبناء.
طبق المكوّنات ذات الترتيب الأعلى بدلًا من ذلك خارج تعريف المكوّن بحيث ينشأ المكوّن الناتج مرة واحدة فقط، بعدها ستكون هويته ثابتة عبر التصييرات. وهذا هو ما تريده عادةً على أيّة حال.
في تلك الحالات النادرة التي تحتاج فيها إلى تطبيق المكوّنات ذات الترتيب الأعلى بشكل ديناميكي فبإمكانك أيضًا فعل ذلك بداخل توابع دورة حياة المكوّن أو دالته البانية.
يجب نسخ التوابع الثابتة
من المفيد أحيانًا تعريف تابع ثابت (static) في مكوّن React. فمثلًا تعرض الحاويات تابعًا ثابتًا يُدعى getFragment
لتسهيل تركيب أجزاء GraphQL.
عند تطبيق المكوّنات ذات الترتيب الأعلى على المكوّن، فسيُغلَّف المكوّن بمكوّن حاوي له. يعني هذا عدم امتلاك المكوّن الجديد لأي من التوابع الثابتة للمكوّن الأصلي:
// تعريف تابع ثابت
WrappedComponent.staticMethod = function() {/*...*/}
// طبق الآن المكون ذو الترتيب الأعلى
const EnhancedComponent = enhance(WrappedComponent);
// يجب ألا يمتلك المكون EnhancedComponent أي تابع ثابت
typeof EnhancedComponent.staticMethod === 'undefined' // true
لحل هذه المشكلة بإمكانك نسخ التوابع إلى الحاوية قبل إعادتها:
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// يجب أن تعلم تمامًا أي توابع يجب نسخها
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
ولكن يتطلب هذا معرفة أي توابع تحتاج إلى نسخها. بإمكانك استخدام هذه الإضافة لنسخ جميع التوابع الثابتة غير المتعلقة بمكتبة React:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
من الحلول الممكنة الأخرى هي استخراج التابع الثابت بشكل منفصل من المكوّن نفسه:
// بدلًا من كتابة ما يلي
MyComponent.someFunction = someFunction;
export default MyComponent;
// استخرج التابع بشكل منفصل
export { someFunction };
// وفي الوحدة المستهلك استورد كليهما معًا
import MyComponent, { someFunction } from './MyComponent.js';
لا تستطيع تمرير المراجع
بينما يكون الغرض من استخدام المكوّنات ذات الترتيب الأعلى هو تمرير كافة الخاصيّات للكائن المُغلَّف فلا يعمل هذا بالنسبة للمراجع. وهذا بسبب عدم كونها خاصيّة مثل المفتاح key. حيث تتعامل معها React بشكلٍ خاص. إن أضفت مرجع ref إلى عنصر مكوّنه ناتج عن مكوّن ذو ترتيب أعلى، فسيشير المرجع إلى نسخة عن المكوّن الحاوي وليس المكوّن المُغلَّف.
حل هذه المشكلة هو استخدام React.forwardRef
(المقدمة في إصدار React 16.3). تعلم المزيد حولها في قسم تمرير المراجع.
انظر أيضًا
- شرح JSX بالتفصيل
- التحقق من الأنواع الثابتة
- التحقق من الأنواع باستخدام PropTypes
- استخدام المراجع مع DOM
- المكونات غير المضبوطة
- تحسين الأداء
- React بدون ES6
- React بدون JSX
- المطابقة (Reconciliation)
- استخدام السياق (Context) في React
- استخدام الأجزاء (Fragments) في React
- المداخل (Portals) في React
- حدود الأخطاء
- مكونات الويب
- تمرير المراجع
- خاصيات التصيير
- تكامل React مع المكتبات الأخرى
- سهولة الوصول
- تقسيم الشيفرة
- الوضع الصارم (Strict Mode)