تمرير الدوال إلى المكونات في React

من موسوعة حسوب

كيف يمكنني تمرير مُعالِج حدث (مثل onClick) إلى مكوّن؟

مرِّر مُعالِجات الأحداث والدوال الأخرى كخاصيّات props إلى المكوّنات الأبناء:

<button onClick={this.handleClick}>

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

كيف أربط الدالة إلى نسخة المكوّن؟

هنالك عدة طرق للتأكّد من أنّ الدوال تستطيع الوصول إلى خاصيّات المكوّن مثل this.props و this.state، بناءً على الصياغة وخطوات البناء التي تستخدمها.

الربط في الدالة البانية (ES2015)

class Foo extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log('حدثت نقرة');
  }
  render() {
    return <button onClick={this.handleClick}>انقر هنا</button>;
  }
}

خاصيّات الصنف (اقتراح المرحلة 3)

class Foo extends Component {
  // ملاحظة: هذه الصياغة تجريبية وليست معيارية بعد
  handleClick = () => {
    console.log('حدثت نقرة');
  }
  render() {
    return <button onClick={this.handleClick}>انقر هنا</button>;
  }
}

الربط في تابع التصيير render

class Foo extends Component {
  handleClick() {
    console.log('حدثت نقرة');
  }
  render() {
    return <button onClick={this.handleClick.bind(this)}>انقر هنا</button>;
  }
}

ملاحظة: يُؤدّي استخدام Function.prototype.bind في التابع render إلى إنشاء دالة جديدة في كل مرّة يُصيَّر فيها المكوّن، ممّا قد يؤثر على الأداء (للمزيد تابع في الأسفل).

استخدام الدوال السهميّة في تابع التصيير render

class Foo extends Component {
  handleClick() {
    console.log('حدثت نقرة');
  }
  render() {
    return <button onClick={() => this.handleClick()}>انقر هنا</button>;
  }
}

ملاحظة: يُؤدّي استخدام الدوال السهميّة في التابع render إلى إنشاء دالة جديدة في كل مرّة يُصيَّر فيها المكوّن، ممّا قد يؤثر على الأداء (للمزيد تابع في الأسفل).

هل من الجيّد استخدام الدوال السهميّة في توابع التصيير؟

بشكلٍ عام نعم، لا مشكلة في ذلك، وهي عادةً الطريقة الأسهل لتمرير المُعامِلات إلى دوال ردود النداء.

إن كانت لديك مشاكل في الأداء، فحاول تحسينه عن طريق الطرق المشروحة في توثيق React.

لماذا من الضروري إجراء الربط أساسًا؟

في JavaScript لا تكون الشيفرتان التاليتان متساويتين:

obj.method();
var method = obj.method;
method();

تضمن توابع الربط عمل الشيفرة الثانية بنفس طريقة عمل الشيفرة الأولى.

تحتاج باستخدام React فقط إلى ربط التوابع التي تُمرِّرها إلى المكوّنات الأخرى. على سبيل المثال يُمرِّر ‎<button onClick={this.handleClick}>‎ التابع this.handleClick لذا تحتاج إلى ربطه، ولكن من غير الضروري ربط التابع render أو توابع دورة حياة المكوّن، فنحن لا نُمرّرها إلى المكوّنات الأخرى.

يشرح هذا المنشور مفهوم الربط (binding) وكيفيّة عمل الدوال في JavaScript بالتفصيل.

لماذا تُستدعى الدالة لدي في كل مرّة يُصيَّر فيها المكوّن؟

تأكّد من عدم استدعاء الدالة عند تمريرها إلى المكوّن:

render() {
  // خطأ: يُستدعى handleClick بدلًا من تمريره كمرجع
  return <button onClick={this.handleClick()}>انقر هنا</button>
}

بدلًا من فعل ذلك مرِّر الدالة نفسها بدون أقواس:

render() {
  // صحيح: تُمرّر الدالة handleClick كمرجع هنا
  return <button onClick={this.handleClick}>انقر هنا</button>
}

كيف أمرّر مُعامِل إلى مُعالِج الأحداث أو رد النداء؟

تستطيع استخدام الدوال السهميّة من أجل الالتفاف حول مُعالِجات الأحداث وتمرير المُعامِلات:

<button onClick={() => this.handleClick(id)} />

يُكافِئ هذا استدعاء ‎.bind:

<button onClick={this.handleClick.bind(this, id)} />

مثال: تمرير المُعامِلات باستخدام الدوال السهميّة

const A = 65 // ASCII حرف

class Alphabet extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.state = {
      justClicked: null,
      letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
    };
  }
  handleClick(letter) {
    this.setState({ justClicked: letter });
  }
  render() {
    return (
      <div>
        Just clicked: {this.state.justClicked}
        <ul>
          {this.state.letters.map(letter =>
            <li key={letter} onClick={() => this.handleClick(letter)}>
              {letter}
            </li>
          )}
        </ul>
      </div>
    )
  }
}

مثال: تمرير المُعامِلات باستخدام خاصيّات البيانات

بإمكانك بشكل بديل استخدام واجهات برمجة التطبيق في DOM لتزين البيانات التي تحتاجها من أجل مُعالِجات الأحداث. اتبع هذه الطريقة إن احتجت لضبط عدد كبير من العناصر أو كنتَ تمتلك شجرة تصيير تعتمد على اختبارات التساوي الخاصّة بالصنف React.PureComponent:

const A = 65 // ASCII حرف

class Alphabet extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.state = {
      justClicked: null,
      letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
    };
  }

  handleClick(e) {
    this.setState({
      justClicked: e.target.dataset.letter
    });
  }

  render() {
    return (
      <div>
        Just clicked: {this.state.justClicked}
        <ul>
          {this.state.letters.map(letter =>
            <li key={letter} data-letter={letter} onClick={this.handleClick}>
              {letter}
            </li>
          )}
        </ul>
      </div>
    )
  }
}

كيف أستطيع منع استدعاء الدالة بسرعة كبيرة أو مرات عديدة؟

إن كان لديك مُعالِج أحداث مثل onClick أو onScroll وكنتَ ترغب في منع إطلاق رد النداء بسرعة كبيرة، فتستطيع تحديد معدّل تنفيذ رد النداء. يُمكِن فعل ذلك باستخدام:

  • تقنية الخنق (throttling): معاينة التغييرات بناءً على تردد معتمد على الوقت (باستخدام ‎_.throttle).
  • منع الارتداد (debouncing): نشر التغييرات بعد مدّة زمنيّة معينة من عدم الفاعليّة (باستخدام ‎_.debounce).
  • الخنق باستخدام requestAnimationFrame : معاينة التغييرات بناءً على requestAnimationFrame (باستخدام raf-schd).

انظر إلى هذا المخطط للمقارنة بين الدالتين throttle و debounce.

ملاحظة: تُزوّدنا الدوال ‎_.debounce، و ‎_.throttle، و raf-schd بتابع للإلغاء cancel لإلغاء ردود النداء المتأخرة. يجب إمّا استدعاء هذا التابع من خلال التابع componentWillUnmount أو التحقق من أنّ المكون لا يزال موصولًا ضمن دالة التأخير.

الخنق (Throttle)

يمنع الخنق استدعاء الدالة أكثر من مرّة ضمن النافذة الزمنيّة المُعطاة. يخنق المثال التالي مُعالِج الأحداث click لمنع استدعائه أكثر من مرّة في الثانية:

import throttle from 'lodash.throttle';

class LoadMoreButton extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.handleClickThrottled = throttle(this.handleClick, 1000);
  }

  componentWillUnmount() {
    this.handleClickThrottled.cancel();
  }

  render() {
    return <button onClick={this.handleClickThrottled}>تحميل المزيد</button>;
  }

  handleClick() {
    this.props.loadMore();
  }
}

منع الارتداد (Debounce)

يضمن منع الارتداد عدم تنفيذ الدالة حتى مرور فترة معينة من الوقت منذ آخر استدعاء لها. يكون هذا مفيدًا عندما يتوجب عليك إجراء بعض الحسابات المكلفة استجابةً لحدث قد ينتهي بسرعة (مثل النزول بالصفحة scroll أو أحداث لوحة المفاتيح). يمنع المثال التالي الارتداد في حقل إدخال نصي مع تأخير 250 ميلي ثانية:

import debounce from 'lodash.debounce';

class Searchbox extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.emitChangeDebounced = debounce(this.emitChange, 250);
  }

  componentWillUnmount() {
    this.emitChangeDebounced.cancel();
  }

  render() {
    return (
      <input
        type="text"
        onChange={this.handleChange}
        placeholder="ابحث..."
        defaultValue={this.props.value}
      />
    );
  }

  handleChange(e) {
	// تجري React pooling على الأحداث
	// لذا نقرأ القيمة قبل منع الارتداد
	// بإمكاننا بشكل بديل استدعاء event.persist() وتمرير كامل الحدث
	// للمزيد من المعلومات انظر https://wiki.hsoub.com/React/handling_events
    this.emitChangeDebounced(e.target.value);
  }

  emitChange(value) {
    this.props.onChange(value);
  }
}

الخنق باستخدام requestAnimationFrame

إنّ requestAnimationFrame هو عبارة عن طريقة لوضع الدالة في طابور لتنفيذه في المتصفح في الوقت المثالي لتحسين أداء التصيير. تُطلَق الدالة التي توضع في الطابور باستخدام requestAnimationFrame في الإطار الزمني التالي. سيعمل المتصفح بجد لضمان الحصول على 60 إطار في الثانية (60 fps). إن لم يكن المتصفح قادرًا على ذلك فسيحدد عدد الإطارات في الثانية. على سبيل المثال قد يكون جهازك قادر على التعامل فقط مع 30 إطار بالثانية لذا ستحصل على 30 إطار بالثانية فقط. إنّ استخدام requestAnimationFrame للخنق هو تقنية مفيدة تمنع من إجراء أكثر من 60 تحديث في الثانية. إن كنت تجري 100 تحديث في الثانية فسيؤدي ذلك إلى إنشاء عمل إضافي على المتصفح والذي لن يراه المستخدم على أية حال.

ملاحظة: استخدام هذه التقنية سيلتقط فقط آخر قيمة منشورة في الإطار. بإمكانك رؤية مثال حول كيفية عمل هذا الضبط من هنا.

import rafSchedule from 'raf-schd';

class ScrollListener extends React.Component {
  constructor(props) {
    super(props);

    this.handleScroll = this.handleScroll.bind(this);

	// إنشاء دالة جديدة لجدولة التحديثات
    this.scheduleUpdate = rafSchedule(
      point => this.props.onScroll(point)
    );
  }

  handleScroll(e) {
	// عند استقبال حدث scroll جدول تحديثًا
	// إن استقبلنا الكثير من التحديثات ضمن الإطار فسننشر آخر قيمة فقط
    this.scheduleUpdate({ x: e.clientX, y: e.clientY });
  }

  componentWillUnmount() {
	// إلغاء أي تحديثات منتظرة بما أننا سنفصل المكون
    this.scheduleUpdate.cancel();
  }

  render() {
    return (
      <div
        style={{ overflow: 'scroll' }}
        onScroll={this.handleScroll}
      >
        <img src="/my-huge-image.jpg" />
      </div>
    );
  }
}

اختبار حدود معدل تحديث الإطار لديك

عند اختبار حدود معدل تحديث الإطار من المفيد امتلاك القدرة على تمرير الزمن بسرعة. إن كنت تستخدم jest بإمكانك استخدام محاكيات الوقت لتمرير الوقت بسرعة. إن كنت تستخدم الخنق عن طريق requestAnimationFrame فهنالك الأداة raf-stub مفيدة للتحكم بضبط تحريك الإطارات.

انظر أيضًا

مصادر