الفرق بين المراجعتين ل"React/higher order components"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
سطر 102: سطر 102:
 
</syntaxhighlight>المُعامِل الأول هو المكوّن المُغلَّف. يسترجع المُعامِل الثاني البيانات التي تُهمّنا، مع إعطاء مصدر البيانات <code>DataSource</code> والخاصيّات الحاليّة.
 
</syntaxhighlight>المُعامِل الأول هو المكوّن المُغلَّف. يسترجع المُعامِل الثاني البيانات التي تُهمّنا، مع إعطاء مصدر البيانات <code>DataSource</code> والخاصيّات الحاليّة.
  
عند تصيير <code>CommentListWithSubscription</code> و <code>BlogPostWithSubscription</code>، فسيُمرِّر المكوّنان <code>CommentList</code> و <code>BlogPost</code> خاصيّة للبيانات <code>data</code> والتي تحمل أحدث البيانات المستخرجة من <code>DataSource</code>:
+
عند تصيير <code>CommentListWithSubscription</code> و <code>BlogPostWithSubscription</code>، فسيُمرِّر المكوّنان <code>CommentList</code> و <code>BlogPost</code> خاصيّة للبيانات <code>data</code> والتي تحمل أحدث البيانات المستخرجة من <code>DataSource</code>:<syntaxhighlight lang="javascript">
 +
// تأخذ هذه الدالة مكوّن
 +
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} />;
 +
    }
 +
  };
 +
}
 +
 
 +
</syntaxhighlight>لاحظ أنّ المكوّن عالي الترتيب لا يُعدِّل مكوّن حقل الإدخال ولا يستخدم الوراثة لنسخ سلوكه، بل يُركِّب المكوّن الأساسي عن طريق تغليفه في مكوّن حاوية. المكوّن عالي الترتيب هو عبارة عن دالة نقيّة (pure) بدون أي تأُثيرات جانبية إطلاقًا.
 +
 
 +
يستقبل المكوّن المُغلَّف جميع الخاصيّات من الحاوية بالإضافة إلى الخاصيّة الجديدة وهي <code>data</code> والتي يستخدمها لتصيير ناتجه. لا يهتم المكوّن عالي الترتيب بكيفية أو سبب استخدام البيانات، ولا يهتم المكوّن المُغلَّف بمصدر البيانات.
 +
 
 +
بما أنّ <code>withSubscription</code> هو دالة عادية بإمكانك إضافة وسائط لها كما تريد. فقد ترغب مثلًا بجعل اسم الخاصيّة <code>data</code> قابلًا للإعداد، وذلك لعزل المكوّن عالي الترتيب عن المكوّن المُغلِّف له، أو تستطيع قبول وسيط يُعِد <code>shouldComponentUpdate</code> أو مصدر البيانات. كل هذه الإمكانيات متوفرة بسبب امتلاك المكوّن عالي الترتيب السيطرة على كيفيّة تعريف المكوّنات.
 +
 
 +
وكما هو الحال مع المكوّنات يكون العقد بين <code>withSubscription</code> والمكوّن المغلّف معتمد بشكل كامل على الخاصيّات. يجعل هذا من السهل استبدال مكوّن عالي الترتيب بواحد آخر، طالما أنّهما يعطيان نفس الخاصيات للمكوّن المغلّف. قد يكون هذا مفيدًا إن غيرت مكتبة الحصول على البيانات مثلًا.
 +
 
 +
== لا تُعدِّل المكوّن الأصلي بل استخدم التراكيب ==
 +
قاوم رغبة تعديل نموذج المكوّن بداخل المكوّن عالي الترتيب:<syntaxhighlight lang="javascript">
 +
function logProps(InputComponent) {
 +
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
 +
    console.log('Current props: ', this.props);
 +
    console.log('Next props: ', nextProps);
 +
  };
 +
  // حقيقة أننا نعيد حقل الإدخال الأصلي هي تلميح إلى أنّه قد تغيّر
 +
  return InputComponent;
 +
}
 +
 
 +
// سيسجل المكون EnhancedComponent عندما تستقبل الخاصيّات
 +
const EnhancedComponent = logProps(InputComponent);
 +
 
 +
</syntaxhighlight>هنالك بعض المشاكل عند فعل ذلك. أحدها هي عدم القدرة على استخدام مكوّن حقل الإدخال بشكل منفصل عن المكوّن <code>EnhancedComponent</code>. وإن طبقت مكوّن عالي الترتيب آخر إلى المكوّن <code>EnhancedComponent</code> والذي يُعدِّل أيضًا <code>componentWillReceiveProps</code>، فسيتجاوز وظيفة المكوّن عالي الترتيب الأول! لا يعمل المكوّن عالي الترتيب هذا أيضًا مع المكوّنات الدالية لأنّها لا تمتلك توابع دورة الحياة.
 +
 
 +
إنّ تعديل المكوّنات عالية الترتيب ليس أمرًا بسيطًا فيجب معرفة كيفية تنفيذها من أجل تجنب التعارض مع المكوّنات عالية الترتيب الأخرى.
 +
 
 +
بدلًا من تعديل المكوّن عالي الترتيب يجب استخدام التراكيب عن طريق تغليف مكوّن حقل الإدخال في مكوّن حاوية:<syntaxhighlight lang="javascript">
 +
function logProps(WrappedComponent) {
 +
  return class extends React.Component {
 +
    componentWillReceiveProps(nextProps) {
 +
      console.log('Current props: ', this.props);
 +
      console.log('Next props: ', nextProps);
 +
    }
 +
    render() {
 +
  // تغليف مكوّن حقل الإدخال في حاوية بدون تعديله
 +
      return <WrappedComponent {...this.props} />;
 +
    }
 +
  }
 +
}
 +
 
 +
</syntaxhighlight>يمتلك هذا المكوّن عالي الترتيب نفس وظيفة نسخة التعديل مع تجنب الأخطاء المحتملة، ويعمل بشكل متكافئ مع مكوّنات الأصناف والدوال. وبما أنّه دالة نقيّة فهو قابل للتركيب مع مكوّنات عالية الترتيب الأخرى أو حتى مع نفسه.
 +
 
 +
ربما قد لاحظت التشابه بين المكوّنات عالية الترتيب وبين النمط الذي يُدعى المكوّنات الحاوية (container components) والتي هي جزء من استراتيجية فصل المسؤولية بين الاهتمامات ذات المستوى الأعلى والاهتمامات ذات المستوى الأدنى. تُدير الحاويات أشياء مثل الاشتراكات والحالة وتُمرِّر خاصيّات للمكوّنات والتي تتعامل مع أشياء مثل تصيير واجهة المستخدم. تستخدم المكوّنات عالية الترتيب الحاويات كجزء منها. بإمكانك النظر إلى المكوّنات عالية الترتيب كتعاريف للمكوّنات الحاوية.
 +
 
 +
== تمرير الخاصيات غير المرتبطة إلى المكون المغلف ==
 +
تُضيف المكوّنات عالية الترتيب ميزات إلى المكوّن. ولكنها لا يجب عليها تغييره. من المتوقع أن يمتلك المكوّن العائد من المكوّن العالي الترتيب نفس الواجهة للمكوّن المغلف له.
 +
 
 +
يجب على المكوّنات عالية الترتيب تمرير الخاصيّات غير المرتبطة بأي اهتمام محدّد. تحتوي معظم المكوّنات عالية الترتيب على تابع للتصيير والذي يبدو مشابهًا لما يلي:<syntaxhighlight lang="javascript">
 +
render() {
 +
  // ترشيح خاصيات إضافية مخصصة لهذا المكون عالي الترتيب والتي لا يجب تمريرها
 +
  const { extraProp, ...passThroughProps } = this.props;
 +
 
 +
  // حقن الخاصيات في المكون المغلف. وهي عادة قيم الحالة أو نسخ من التوابع
 +
 
 +
  const injectedProp = someStateOrInstanceMethod;
 +
 
 +
  // تمرير الخاصيات إلى المكون المغلف
 +
 
 +
  return (
 +
    <WrappedComponent
 +
      injectedProp={injectedProp}
 +
      {...passThroughProps}
 +
    />
 +
  );
 +
}
 +
 
 +
</syntaxhighlight>يضمن هذا أن تكون المكوّنات عالية الترتيب مرنة وقابلة لإعادة الاستخدام قدر الإمكان.
 +
 
 +
== رفع إمكانية التركيب إلى أقصى درجة ==
 +
لا تبدو كافة المكونات عالية الترتيب مثل بعضها. فأحيانًا تقبل فقط وسيط واحد، وهو المكون المغلف:

مراجعة 08:36، 24 أغسطس 2018

إنّ المكوّنات ذات الترتيب الأعلى (Higher-Order Components - React واختصارًا 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.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // حقيقة أننا نعيد حقل الإدخال الأصلي هي تلميح إلى أنّه قد تغيّر
  return InputComponent;
}

// سيسجل المكون EnhancedComponent عندما تستقبل الخاصيّات
const EnhancedComponent = logProps(InputComponent);

هنالك بعض المشاكل عند فعل ذلك. أحدها هي عدم القدرة على استخدام مكوّن حقل الإدخال بشكل منفصل عن المكوّن EnhancedComponent. وإن طبقت مكوّن عالي الترتيب آخر إلى المكوّن EnhancedComponent والذي يُعدِّل أيضًا componentWillReceiveProps، فسيتجاوز وظيفة المكوّن عالي الترتيب الأول! لا يعمل المكوّن عالي الترتيب هذا أيضًا مع المكوّنات الدالية لأنّها لا تمتلك توابع دورة الحياة.

إنّ تعديل المكوّنات عالية الترتيب ليس أمرًا بسيطًا فيجب معرفة كيفية تنفيذها من أجل تجنب التعارض مع المكوّنات عالية الترتيب الأخرى.

بدلًا من تعديل المكوّن عالي الترتيب يجب استخدام التراكيب عن طريق تغليف مكوّن حقل الإدخال في مكوّن حاوية:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
	  // تغليف مكوّن حقل الإدخال في حاوية بدون تعديله
      return <WrappedComponent {...this.props} />;
    }
  }
}

يمتلك هذا المكوّن عالي الترتيب نفس وظيفة نسخة التعديل مع تجنب الأخطاء المحتملة، ويعمل بشكل متكافئ مع مكوّنات الأصناف والدوال. وبما أنّه دالة نقيّة فهو قابل للتركيب مع مكوّنات عالية الترتيب الأخرى أو حتى مع نفسه.

ربما قد لاحظت التشابه بين المكوّنات عالية الترتيب وبين النمط الذي يُدعى المكوّنات الحاوية (container components) والتي هي جزء من استراتيجية فصل المسؤولية بين الاهتمامات ذات المستوى الأعلى والاهتمامات ذات المستوى الأدنى. تُدير الحاويات أشياء مثل الاشتراكات والحالة وتُمرِّر خاصيّات للمكوّنات والتي تتعامل مع أشياء مثل تصيير واجهة المستخدم. تستخدم المكوّنات عالية الترتيب الحاويات كجزء منها. بإمكانك النظر إلى المكوّنات عالية الترتيب كتعاريف للمكوّنات الحاوية.

تمرير الخاصيات غير المرتبطة إلى المكون المغلف

تُضيف المكوّنات عالية الترتيب ميزات إلى المكوّن. ولكنها لا يجب عليها تغييره. من المتوقع أن يمتلك المكوّن العائد من المكوّن العالي الترتيب نفس الواجهة للمكوّن المغلف له.

يجب على المكوّنات عالية الترتيب تمرير الخاصيّات غير المرتبطة بأي اهتمام محدّد. تحتوي معظم المكوّنات عالية الترتيب على تابع للتصيير والذي يبدو مشابهًا لما يلي:

render() {
  // ترشيح خاصيات إضافية مخصصة لهذا المكون عالي الترتيب والتي لا يجب تمريرها
  const { extraProp, ...passThroughProps } = this.props;

  // حقن الخاصيات في المكون المغلف. وهي عادة قيم الحالة أو نسخ من التوابع
  
  const injectedProp = someStateOrInstanceMethod;

  // تمرير الخاصيات إلى المكون المغلف
  
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

يضمن هذا أن تكون المكوّنات عالية الترتيب مرنة وقابلة لإعادة الاستخدام قدر الإمكان.

رفع إمكانية التركيب إلى أقصى درجة

لا تبدو كافة المكونات عالية الترتيب مثل بعضها. فأحيانًا تقبل فقط وسيط واحد، وهو المكون المغلف: