خطاف التأثير في React

من موسوعة حسوب
< React
مراجعة 19:42، 13 فبراير 2019 بواسطة جميل-بيلوني (نقاش | مساهمات) (إنشاء الصفحة.)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى التنقل اذهب إلى البحث

الخطافات هي إضافة جديدة إلى الإصدار 16.8 في React، إذ تسمح لك باستعمال ميزة الحالة وميزات React الأخرى دون كتابة أي صنف.

يمكِّن خطاف التأثير (Effect Hook) من إحداث تأثيرات جانبية (side effects) في مكونات دالة:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // componentDidMount و componentDidUpdate يشبه
  useEffect(() => {
    // تحديث عنوان الصفحة باستعمال واجهة المتصفح البرمجية
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

هذا المثال معتمد على مثال العداد المستقى من الصفحة السابقة، ولكن أضفنا إليه ميزة جديدة هي ضبط عنوان الصفحة إلى رسالة مخصصة تتضمن عدد الضغطات.

جلب البيانات، وضبط اشتراك (subscription)، وتغيير شجرة DOM يدويًّا في مكونات React كلها أمثلة عن التأثيرات الجانبية. سواءً أكنت اعتدت على تسمية هذه العمليات "بالتأثيرات الجانبية" (أو "التأثيرات") فقط أم لا، فلا بد أنك قد نفَّذت هذه العمليات في مكوناتك مسبقًا.

نصيحة: إن كانت توابع دورة حياة الأصناف مألوفةً لديك، فيمكنك التفكير بالخطاف useEffect بأنه ناتج دمج componentDidMount و componentDidUpdate و componentWillUnmount.

هنالك نوعان شائعان للتأثيرات الجانبية في مكونات React هما: الأول هو تلك التي لا تتطلب تنفيذ عملية تنظيف، والثاني هو تلك التي تتطلب ذلك. دعنا نطلع عليهما بمزيد من التفصيل.

تأثيرات بدون عملية تنظيف

نريد أحيانًا تنفيذ بعض الشيفرات الإضافية بعد تحديث React شجرة DOM. طلبيات الشبكة (Network requests)، والتعديلات اليدوية على DOM، والتسجيل (logging) هي أمثلةٌ شائعة عن التأثيرات التي لا تتطلب إجراء تنظيف (cleanup)، وذلك لأننا نستطيع تنفيذها ثم نسيان أمرها مباشرةً. إذًا، دعنا نوازن كيف تمكننا الأصناف والخطافات من التعبير عن مثل هذه التأثيرات الجانبية.

مثال باستعمال الأصناف

في مكونات صنف في React، التابع render نفسه لا يجب أن يسبب أي تأثيرات جانبية، إذ يكون ذلك مبكرًا جدًا. نريد عادةً تنفيذ التأثيرات الجانبية الخاصة بنا بعد تحديث React شجرة DOM.

هذا هو سبب وضع التأثيرات الجانبية في أصناف React ضمن componentDidMount و componentDidUpdate. بالعودة إلى مثالنا، إليك مكون صنفٍ عدادٍ في React يحدِّث عنوان الصفحة بعد تنفيذ React التغييرات في DOM:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

لاحظ كيف يتوجب علينا مضاعفة الشيفرة بين دورتي حياة هذين التابعين في الصنف. هذا بسبب أننا نريد في حالات عديدة تنفيذ نفس التأثير الجانبي بغض النظر عن ما إذا كان المكون قد وُصِلَ (mounted) للتو أو إن حُدِّث. نظريًّا، نريد أن يحدث ذلك بعد كل عملية تصيير (render)، ولكن مكونات صنف React لا تملك أي تابع يؤدي هذا الغرض. نستطيع استخراج تابع منفصل ولكن لا يزال يتوجب علينا استدعائه في موضعين.

من جهة أخرى، دعنا نرى كيف نستطيع القيام بالأمر نفسه باستعمال الخطاف useEffect.

مثال باستعمال الخطافات

المثال التالي رأيناه في بداية هذه الصفحة، ولكن دعنا نلقِ نظرة قريبة عليه:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

ما الذي يفعله الخطاف useEffect؟

باستعمال هذا الخطاف، أنت تخبر React بأن مكونك يحتاج إلى تنفيذ أمر ما بعد عملية التصيير. ستتذكر React الدالة التي مررتها (سنشير إليها بأنها "التأثير" [effect] الخاص بنا)، وتستدعيها لاحقًا بعد إجراء تحديث على DOM. نضبط في هذا التأثير عنوان الصفحة، ولكن يمكننا أيضًا تنفيذ عملية جلب بياناتٍ أو استدعاء واجهة برمجية أمرية (imperative API) أخرى.

لماذا يُستدعَى useEffect داخل مكون؟

وضع useEffect داخل المكون يمكننا من الوصول إلى متغير الحالة count (أو أي خاصية) بشكل صحيح من التأثير. لا نحتاج إلى واجهة برمجية خاصة لقراءته، إذ يكون ضمن مجال الدالة مسبقًا. تتبنى الخطافات مفهوم مغلفات جافاسكربت (JavaScript closures) وتتجنب تعريف واجهات React برمجية مخصصة، إذ وفرت جافاسكربت حلًا مسبقًا.

هل يُنفَّذ الخطاف useEffect بعد كل عملية تصيير؟

نعم. افتراضيًّا، يُنفَّذ بعد أول عملية تصيير وبعد كل عملية تحديث. (سنتحدث لاحقًا عن كيفية تخصيص ذلك.) بدلًا من التفكير من ناحية الوصل (mounting) والتحديث (updating)، ربما تجد أنه من السهل التفكير بأن تلك التأثيرات تحدث "بعد التصيير". تضمن React بأن شجرة DOM قد حُدِّثَت في الوقت الذي تُنفِّذ فيه التأثيرات.

تفصيل أوسع

الآن وبعد التعرف على التأثيرات، يجب أن تكون أسطر الشيفرة التالية مفهومة:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

صرَّحنا عن متغير الحالة count، وأخبرنا بعدئذٍ React أننا نحتاج إلى استعمال تأثير. مرَّرنا دالةً إلى الخطاف useEffect والتي تمثِّل التأثير الخاص بنا. داخل هذا التأثير، ضبطنا عنوان الصفحة باستعمال الواجهة البرمجية document.title للمتصفح. يمكننا الآن قراءة أحدث قيمة للمتغير count داخل التأثير لأنه يتوضع داخل نطاق الدالة. عندما تصيِّر React المكون الخاص بنا، ستتذكر التأثير الذي استعملناه ثم ستُنفِّذ هذا التأثير بعد تحديث DOM. هذا يحدث من أجل كل عملية تصيير بما فيها عملية التصيير الأولى.

مطورو جافاسكربت المتمرسون قد يلاحظون أن الدالة التي مررناها إلى الخطاف useEffect ستكون مختلفة في كل عملية تصيير. صحيح، فهذا الأمر متعمَّد، إذ هذا، في الحقيقة، هو الذي يمكننا من قراءة قيمة count من داخل التأثير دون القلق من تقادمها. في كل مرة نكرر فيها عملية التصيير، نجدول تأثيرًا جديدًا باستبدال التأثير السابق. بطريقةٍ ما، هذا يجعل التأثير يتصرف بطريقة أشبه بكونه جزءًا من ناتج عملية التصيير، إذ كل تأثير "يتبع" إلى عملية تصيير محدَّدة. سنلقِ نظرة تفصيلية عن سبب كون هذا السلوك مفيدًا في قسم لاحق من هذه الصفحة.

نصيحة: بخلاف componentDidMount أو componentDidUpdate، التأثيرات المجدولة مع useEffect لا تحجز المتصفح من تحديث الصفحة مما يزيد من تجاوبية تطبيقك.أغلبية التأثيرات لا تحتاج إلى تحدث بشكل متزامن. في الحالات النادرة التي تحدث فيها (مثل ضبط التخطيط [measuring the layout])، يوجد خطاف منفصل يدعى useLayoutEffect مع واجهة برمجية مماثلة للخطاف useEffect.

تأثيرات مع عملية تنظيف

فيما سبق، اطلعنا على كيفية عمل التأثيرات الجانبية التي لا تتطلب أية إجراءات تنظيف. على أي حال، هنالك بعض التأثيرات التي تتطلب ذلك: على سبيل المثال، ربما كنا نريد ضبط اشتراك إلى بعض موارد البيانات الخارجية. في هذه الحالة، من الضروري إجراء عملية تنظيف، وبذلك لا نتسبب في حدوث تسريب في الذاكرة (memory leak). دعنا نوازن كيفية القيام بذلك مع الأصناف ومع الخطافات.

مثال باستعمال الأصناف

في صنف React، ستحتاج عادةً إلى ضبط أي اشتراك في componentDidMount، وإجراء عملية التنظيف في componentWillUnmount. على سبيل المثال، دعنا نفترض أنه لدينا الوحدة ChatAPI التي تمكننا من الاشتراك بحالة اتصال صديق (friend’s online status). إليك كيفية إجراء ذلك وإظهار تلك الحالة باستعمال صنف:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

لاحظ كيف يحتاج componentDidMount و componentWillUnmount إلى أن يعكس كل منهما الآخر. تجبرنا توابع دورة الحياة (Lifecycle methods) بفصل هذه الشيفرة رغم أن الشيفرة من الناحية النظرية في كليهما متعلقة بنفس التأثير.

ملاحظة: قد تلاحظ عمليات التصيير الدقيقة (Eagle-eyed readers) أن هذا المثال يحتاج أيضًا إلى التابع componentDidUpdate ليكون صحيحًا بالمجمل.سنتغاضى عن هذا الأمر الآن ولكم سنعود إليه في قسم أخر لاحق من هذه الصفحة.

مثال باستعمال الخطافات

دعنا نرى كيف يمكننا كتابة هذا المكون باستعمال الخطافات.

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

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // يحدد كيفية إجراء عملية التنظيف بعد هذا التأثير
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

لماذا أعدنا دالة من التأثير؟

هذا السلوك هو آلية اختيارية لإجراء عملية تنظيف للتأثيرات.قد يعيد كل تأثير دالةً تجري عملية تنظيف خلفه. هذا يمكننا من إبقاء شيفرة إضافة وإزالة الاشتراكات قريبةً من بعضها بعضًا، إذ كل منهما جزءٌ من التأثير نفسه.

متى تجري React عملية تنظيف للتأثير؟

تنفِّذ React عملية التنظيف عندما يجري فصل (unmount) المكون. على أي حال وكما تعلمنا سابقًا، تُنفَّذ التأثيرات في كل عملية تصيير وليس مرةً واحدةً. هذا هو سبب تنفيذ React عملية تنظيف للتأثيرات أيضًا من عملية التصيير السابقة قبل تنفيذ التأثير في المرة اللاحقة.سنناقش لاحقًا سبب مساعدة هذا الأمر بتجب حصول أخطاء وكيفية تجنب هذا السلوك في حال تسبب بمشاكل متعلقة بالأداء.

ملاحظة: لا يتوجب علينا إعادة دالة مسماة من التأثير. لقد أطلقنا عليها cleanup هنا لتوضيح وظيفتها، ولكن يمكنك أن تعيد دالة سهمية أو تطلق عليها أي اسم آخر.

الخلاصة

تعلمنا إلى الآن أن الخطاف useEffect يمكننا من التعبير عن مختلف أنواع التأثيرات الجانبية بعد تنفيذ عمليات تصيير لمكون. بعض التأثيرات قد تتطلب إجراء عملية تنظيف، لذا تعيد دالة:

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

بينما قد لا تتطلب تأثيرات أخرى إجراء عملية تنظيف، فلا تعيد آنذاك أي شيء:

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

يوحد خطاف التأثير كلا الحالتين السابقتين في واجهة برمجية واحدة.

تنبيه: إن كنت تشعر أن لديك معرفة جيدة بكيفية عمل خطافات التأثير، أو إن كنت تشعر بالإرهاق والغرق في كم هائل من المعلومات، فيمكنك الآن الانتقال إلى الصفحة التالية: "قواعد استعمال الخطافات".

نصائح لاستعمال التأثيرات

سنكمل هذه الصفحة بإلقاء نظرة تفصيلية على بعض نواحي الخطاف useEffect التي قد يكون مستخدمو React الخبيرون متشوقين للاطلاع عليها. لا تجبر نفسك بالاطلاع عليها الآن. يمكنك في أي وقت العودة إلى هذه الصفحة لتعلم تفاصيل أوسع عن خطاف التأثير.

استعمال تأثيرات متعددة لفصل الشيفرات ذات الصلة

إحدى المشكلات التي أشرنا إليها في قسم "الحافز لإضافة الخطافات" هي أن توابع دورة حياة صنف تحوي غالبًا على شيفرة غير مترابطة، ولكن الشيفرة المترابطة تقسَّم إلى توابع متعددة. إليك مكون يدمج العداد ومؤشر حالة الصديق من الأمثلة السابقة:

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

لاحظ كيف أن الشيفرة التي تضبط document.title تنقسم بين componentDidMount و componentDidUpdate. شيفرة الاشتراك تتوزع أيضًا على componentDidMount و componentWillUnmount. ويحوي componentDidMount شيفرة لكلا المهمتين. لذلك، كيف يمكن للخطافات أن تحل هذه المشكلة؟ بشكل مشابه لتمكنك من استعمال خطاف الحالة أكثر من مرة واحدة، يمكنك أيضًا استعمال عدة تأثيرات. هذا يمكِّننا من فصل الشيفرة الغير مترابطة إلى تأثيرات مختلفة:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}

تسمح الخطافات بفصل الشيفرة بناءً على وظيفتها بدلًا من اسم تابع دورة الحياة. ستطبق React كل تأثير استُعمِل عبر المكون وفقًا للترتيب الذي حدِّدت به.

لماذا تُنفَّذت التأثيرات على كل تحديث

إن كنت معتادًا على الأصناف، فقد تتساءل عن سبب حدوث مرحلة التنظيف في التأثيرات بعد كل عملية إعادة تصيير، وليس مرةً واحدةً أثناء الفصل (unmounting). دعنا نشرح بمثال عملي لماذا يساعدنا هذا السلوك بإنشاء مكونات مع نسبة أخطاء أقل.

فيما سبق من هذه الصفحة، عرَّفنا مثالًا يحوي المكون FriendStatus الذي يظهر فيما إذا كانت حالة صديق "متصل" أم "غير متصل". يقرأ الصنف الخاص بنا الخاصية friend.id من this.props، ثم يشترك بحال الصديق بعد وصل المكون، ثم يلغي الاشتراك أثناء عملية الفصل:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

لكن ماذا يحدث إن تغيرت خاصية الصديق أثناء عرض المكون على الشاشة. سيستمر المكون الخاص بنا بإظهار حالة الاتصال لصديق مختلف، وهذا بالتأكيد خطأ. أضف إلى ذلك أننا سنتسبب بحدوث تسريب في الذاركة أو إنهيار العملية أثناء الفصل، إذ استدعاء إلغاء الاشتراك سيستعمل معرِّف صديق خطأ. في مكون صنف، سنحتاج إلى إضافة componentDidUpdate لمعالجة هذه الحالة:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // السابق friend.id إلغاء الاشتراك من المعرف
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // اللاحق friend.id الاشتراك بالمعرف
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

نسيان المعالج componentDidUpdate هو مصدر شائع لحصول أخطاء منطقية في تطبيقات React. افترض الآن نسخة أخرى مشابهة لهذا المكون ولكن تستعمل الخطافات:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

لا تحوي هذه الشيفرة أي أثر للخطأ السابق. (ولكن لم نجرِ أية تغييرات لها في نفس الوقت.) ليس هنالك أية شيفرة خاصة لمعالجة التحديثات بسبب أن الخطاف useEffect يعالجها افتراضيًّا، إذ ينظف التأثيرات السابقة قبل تطبيق التأثيرات اللاحقة. لتوضيح هذا الأمر، إليك سلسلة من استدعاءات الاشتراك وإلغاء الاشتراك التي يمكن للمكون أن يولدها مع مرور الوقت:

// { friend: { id: 100 } } الوصل مع الخاصيات
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// { friend: { id: 200 } } التحديث مع الخاصيات
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// { friend: { id: 300 } } التحديث مع الخاصيات
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// الفصل
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

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

انظر أيضًا

 مصادر