الفرق بين المراجعتين لصفحة: «React/hooks effect»

من موسوعة حسوب
إكمال إضافة محتوى الصفحة
تحديث
 
(مراجعة متوسطة واحدة بواسطة مستخدم واحد آخر غير معروضة)
سطر 32: سطر 32:


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


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


من جهة أخرى، دعنا نرى كيف نستطيع القيام بالأمر نفسه باستعمال الخطاف <code>useEffect</code>.
من جهة أخرى، دعنا نرى كيف نستطيع القيام بالأمر نفسه باستعمال الخطاف <code>useEffect</code>.
سطر 91: سطر 91:
</syntaxhighlight>'''ما الذي يفعله الخطاف <code>useEffect</code>؟'''
</syntaxhighlight>'''ما الذي يفعله الخطاف <code>useEffect</code>؟'''


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


'''لماذا يُستدعَى <code>useEffect</code> داخل مكون؟'''
'''لماذا يُستدعَى <code>useEffect</code> داخل مكون؟'''


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


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


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


=== تفصيل أوسع ===
=== تفصيل أوسع ===
الآن وبعد التعرف على التأثيرات، يجب أن تكون أسطر الشيفرة التالية مفهومة:<syntaxhighlight lang="javascript">
الآن وبعد التعرف على التأثيرات، يجب أن تصبح أسطر الشيفرة التالية مفهومةً لك:<syntaxhighlight lang="javascript">
function Example() {
function Example() {
   const [count, setCount] = useState(0);
   const [count, setCount] = useState(0);
سطر 109: سطر 109:
     document.title = `You clicked ${count} times`;
     document.title = `You clicked ${count} times`;
   });
   });
</syntaxhighlight>صرَّحنا عن متغير الحالة <code>count</code>، وأخبرنا بعدئذٍ [[React]] أننا نحتاج إلى استعمال تأثير. مرَّرنا دالةً إلى الخطاف <code>useEffect</code> والتي تمثِّل التأثير الخاص بنا. داخل هذا التأثير، ضبطنا عنوان الصفحة باستعمال الواجهة البرمجية <code>document.title</code> للمتصفح. يمكننا الآن قراءة أحدث قيمة للمتغير <code>count</code> داخل التأثير لأنه يتوضع داخل نطاق الدالة. عندما تصيِّر [[React]] المكون الخاص بنا، ستتذكر التأثير الذي استعملناه ثم ستُنفِّذ هذا التأثير بعد تحديث DOM. هذا يحدث من أجل كل عملية تصيير بما فيها عملية التصيير الأولى.
</syntaxhighlight>صرَّحنا عن متغير الحالة <code>count</code>، وأخبرنا بعدئذٍ [[React]] أننا نحتاج إلى استعمال تأثير. مرَّرنا دالةً إلى الخطاف <code>useEffect</code> والتي تمثِّل التأثير الخاص بنا. داخل هذا التأثير، ضبطنا عنوان الصفحة باستعمال الواجهة البرمجية <code>document.title</code> للمتصفح. يمكننا الآن قراءة أحدث قيمة للمتغير <code>count</code> داخل التأثير لأنَّه يتوضع داخل نطاق الدالة. عندما تصيِّر [[React]] المكون الخاص بنا، ستتذكر التأثير الذي استعملناه ثم ستُنفِّذ هذا التأثير بعد تحديث DOM. هذا يحدث من أجل كل عملية تصيير بما فيها عملية التصيير الأولى.


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


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


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


=== مثال باستعمال الأصناف ===
=== مثال باستعمال الأصناف ===
في صنف [[React]]، ستحتاج عادةً إلى ضبط أي اشتراك في <code>componentDidMount</code>، وإجراء عملية التنظيف في <code>componentWillUnmount</code>. على سبيل المثال، دعنا نفترض أنه لدينا الوحدة <code>ChatAPI</code> التي تمكننا من الاشتراك بحالة اتصال صديق (friend’s online status). إليك كيفية إجراء ذلك وإظهار تلك الحالة باستعمال صنف:<syntaxhighlight lang="javascript">
في صنف [[React]]، ستحتاج عادةً إلى ضبط أي اشتراك في <code>componentDidMount</code>، وإجراء عملية التنظيف في <code>componentWillUnmount</code>. على سبيل المثال، دعنا نفترض أنَّه لدينا الوحدة <code>ChatAPI</code> التي تمكننا من الاشتراك بحالة اتصال صديق (friend’s online status). إليك كيفية إجراء ذلك وإظهار تلك الحالة باستعمال صنف:<syntaxhighlight lang="javascript">
class FriendStatus extends React.Component {
class FriendStatus extends React.Component {
   constructor(props) {
   constructor(props) {
سطر 133: سطر 133:
     );
     );
   }
   }
   componentWillUnmount() {
   componentWillUnmount() {
     ChatAPI.unsubscribeFromFriendStatus(
     ChatAPI.unsubscribeFromFriendStatus(
سطر 140: سطر 139:
     );
     );
   }
   }
   handleStatusChange(status) {
   handleStatusChange(status) {
     this.setState({
     this.setState({
سطر 154: سطر 152:
   }
   }
}
}
</syntaxhighlight>لاحظ كيف يحتاج <code>componentDidMount</code> و <code>componentWillUnmount</code> إلى أن يعكس كل منهما الآخر. تجبرنا توابع دورة الحياة (Lifecycle methods) بفصل هذه الشيفرة رغم أنَّ الشيفرة من الناحية النظرية في كليهما متعلقة بنفس التأثير.
</syntaxhighlight>لاحظ كيف يحتاج <code>componentDidMount</code> و <code>componentWillUnmount</code> إلى أن يعكس كل منهما الآخر. تجبرنا توابع دورة الحياة (Lifecycle methods) بفصل هذه الشيفرة رغم أنَّ الشيفرة من الناحية النظرية في كليهما متعلقة بنفس التأثير.


سطر 186: سطر 185:
</syntaxhighlight>'''لماذا أعدنا دالة من التأثير؟'''
</syntaxhighlight>'''لماذا أعدنا دالة من التأثير؟'''


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


'''متى تجري React عملية تنظيف للتأثير؟'''
'''متى تجري React عملية تنظيف للتأثير؟'''
سطر 195: سطر 194:


== الخلاصة ==
== الخلاصة ==
تعلمنا إلى الآن أنَّ الخطاف <code>useEffect</code> يمكِّننا من التعبير عن مختلف أنواع التأثيرات الجانبية بعد تنفيذ عمليات تصيير لمكون. بعض التأثيرات قد تتطلب إجراء عملية تنظيف، لذا تعيد دالة:<syntaxhighlight lang="javascript">
تعلمنا إلى الآن أنَّ الخطاف <code>useEffect</code> يمكِّننا من التعبير عن مختلف أنواع التأثيرات الجانبية بعد تنفيذ عمليات تصيير لمكون. قد تتطلب بعض التأثيرات إجراء عملية تنظيف، لذا تعيد دالة:<syntaxhighlight lang="javascript">
   useEffect(() => {
   useEffect(() => {
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
سطر 214: سطر 213:


=== استعمال تأثيرات متعددة لفصل الشيفرات ذات الصلة ===
=== استعمال تأثيرات متعددة لفصل الشيفرات ذات الصلة ===
إحدى المشكلات التي أشرنا إليها في قسم "[[React/hooks intro#.D8.A7.D9.84.D9.85.D9.83.D9.88.D9.86.D8.A7.D8.AA .D8.A7.D9.84.D9.85.D8.B9.D9.82.D8.AF.D8.A9 .D8.AA.D8.B5.D8.A8.D8.AD .D8.B5.D8.B9.D8.A8.D8.A9 .D8.A7.D9.84.D9.81.D9.87.D9.85|الحافز لإضافة الخطافات]]" هي أن توابع دورة حياة صنف تحوي غالبًا على شيفرة غير مترابطة، ولكن الشيفرة المترابطة تقسَّم إلى توابع متعددة. إليك مكون يدمج العداد ومؤشر حالة الصديق من الأمثلة السابقة:<syntaxhighlight lang="javascript">
إحدى المشكلات التي أشرنا إليها في قسم "[[React/hooks intro#.D8.A7.D9.84.D9.85.D9.83.D9.88.D9.86.D8.A7.D8.AA .D8.A7.D9.84.D9.85.D8.B9.D9.82.D8.AF.D8.A9 .D8.AA.D8.B5.D8.A8.D8.AD .D8.B5.D8.B9.D8.A8.D8.A9 .D8.A7.D9.84.D9.81.D9.87.D9.85|الحافز لإضافة الخطافات]]" هي أنَّ توابع دورة حياة صنف تحوي غالبًا شيفرةً غير مترابطة، ولكنَّ الشيفرة المترابطة تقسَّم إلى توابع متعددة. إليك مكون يدمج العداد ومؤشر حالة الصديق من الأمثلة السابقة:<syntaxhighlight lang="javascript">
class FriendStatusWithCounter extends React.Component {
class FriendStatusWithCounter extends React.Component {
   constructor(props) {
   constructor(props) {
سطر 247: سطر 246:
   }
   }
   // ...
   // ...
</syntaxhighlight>لاحظ كيف أنَّ الشيفرة التي تضبط <code>document.title</code> تنقسم بين <code>componentDidMount</code> و <code>componentDidUpdate</code>. شيفرة الاشتراك تتوزع أيضًا على <code>componentDidMount</code> و <code>componentWillUnmount</code>. ويحوي <code>componentDidMount</code> شيفرة لكلا المهمَّتين.
</syntaxhighlight>لاحظ كيف أنَّ الشيفرة التي تضبط <code>document.title</code> تنقسم بين <code>componentDidMount</code> و <code>componentDidUpdate</code>. شيفرة الاشتراك تتوزع أيضًا على <code>componentDidMount</code> و <code>componentWillUnmount</code>. ويحوي <code>componentDidMount</code> شيفرة لكلا المهمَّتين.


سطر 258: سطر 258:
   const [isOnline, setIsOnline] = useState(null);
   const [isOnline, setIsOnline] = useState(null);
   useEffect(() => {
   useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
     return () => {
     return () => {
سطر 263: سطر 267:
     };
     };
   });
   });
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
   // ...
   // ...
}
}
</syntaxhighlight>تسمح الخطافات بفصل الشيفرة بناءً على وظيفتها بدلًا من اسم تابع دورة الحياة. ستطبق [[React]] كل تأثير استُعمِل عبر المكون وفقًا للترتيب الذي حدِّدت به.
 
</syntaxhighlight>تسمح الخطافات بفصل الشيفرة بناءً على وظيفتها بدلًا من اسم تابع دورة الحياة. ستطبق [[React]] كل تأثير استُعمِل عبر المكون وفقًا للترتيب الذي عُرِّف به.


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


فيما سبق من هذه الصفحة، عرَّفنا مثالًا يحوي المكون <code>FriendStatus</code> الذي يظهر فيما إذا كانت حالة صديق "متصل" أم "غير متصل". يقرأ الصنف الخاص بنا الخاصية <code>friend.id</code> من <code>this.props</code>، ثم يشترك بحال الصديق بعد وصل المكون، ثم يلغي الاشتراك أثناء عملية الفصل:<syntaxhighlight lang="javascript">
عرَّفنا آنفا مثالًا يحوي المكون <code>FriendStatus</code> الذي يظهر إذا كانت حالة صديق "متصل" أم "غير متصل". يقرأ الصنف الخاص بنا الخاصية <code>friend.id</code> من <code>this.props</code>، ثم يشترك بحال الصديق بعد وصل المكون، ثم يلغي الاشتراك أثناء عملية الفصل:<syntaxhighlight lang="javascript">
   componentDidMount() {
   componentDidMount() {
     ChatAPI.subscribeToFriendStatus(
     ChatAPI.subscribeToFriendStatus(
سطر 288: سطر 289:
     );
     );
   }
   }
</syntaxhighlight>لكن ماذا يحدث إن تغيرت خاصية الصديق أثناء عرض المكون على الشاشة. سيستمر المكون الخاص بنا بإظهار حالة الاتصال لصديق مختلف، وهذا بالتأكيد خطأ. أضف إلى ذلك أننا سنتسبب بحدوث تسريب في الذاكرة أو انهيار العملية أثناء الفصل، إذ استدعاء إلغاء الاشتراك سيستعمل معرِّف صديق خطأ.
</syntaxhighlight>لكن ماذا يحدث إن تغيَّرت خاصية الصديق أثناء عرض المكون على الشاشة. سيستمر المكون الخاص بنا بإظهار حالة الاتصال لصديق مختلف، وهذا بالتأكيد خطأ. أضف إلى ذلك أنَّنا سنتسبَّب بحدوث تسريب في الذاكرة أو انهيار العملية أثناء الفصل، إذ سيستعمل استدعاء إلغاء الاشتراك معرِّف صديق خطأ.


في مكون صنف، سنحتاج إلى إضافة <code>componentDidUpdate</code> لمعالجة هذه الحالة:<syntaxhighlight lang="javascript">
في مكون صنف، سنحتاج إلى إضافة <code>componentDidUpdate</code> لمعالجة هذه الحالة:<syntaxhighlight lang="javascript">
سطر 346: سطر 347:
</syntaxhighlight>يتأكد هذا السلوك تناسق وتطابق الاستدعاءات في الشيفرة بشكل افتراضي ويمنع حصول أية أخطاء شائعة الظهور في مكونات الصنف نتيجة نسيان عملية التحديث.
</syntaxhighlight>يتأكد هذا السلوك تناسق وتطابق الاستدعاءات في الشيفرة بشكل افتراضي ويمنع حصول أية أخطاء شائعة الظهور في مكونات الصنف نتيجة نسيان عملية التحديث.


=== تحسن الأداء عبر تخطي التأثيرات ===
=== تحسين الأداء عبر تخطي التأثيرات ===
في بعض الحالات، تنظيف أو تطبيق التأثير بعد كل عملية تصيير قد يولد مشكلة في الأداء. في مكونات صنف، يمكننا حل هذه المشكلة عبر كتابة موازنة إضافية باستعمال <code>prevProps</code> أو <code>prevState</code> داخل <code>componentDidUpdate</code>:<syntaxhighlight lang="javascript">
في بعض الحالات، تنظيف أو تطبيق التأثير بعد كل عملية تصيير قد يبطِّئ الأداء. في مكونات صنف، يمكننا حل هذه المشكلة عبر كتابة موازنة إضافية باستعمال <code>prevProps</code> أو <code>prevState</code> داخل <code>componentDidUpdate</code>:<syntaxhighlight lang="javascript">
componentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
   if (prevState.count !== this.state.count) {
   if (prevState.count !== this.state.count) {
سطر 357: سطر 358:
   document.title = `You clicked ${count} times`;
   document.title = `You clicked ${count} times`;
}, [count]); // فقط count أعد تنفيذ التأثير إن تغير
}, [count]); // فقط count أعد تنفيذ التأثير إن تغير
</syntaxhighlight>في المثال أعلاه، مرَّرنا <code>[count]</code> كوسيط ثانٍ. ولكن، ما الذي يعني تمرير مثل هذه القيمة؟ إن أصبحت قيمة المتغير <code>count</code> هي 5، وأُعيد تصيير المكون مع المتغير <code>count</code> الذي لا تزال قيمته ثابتة (أي 5)، فستوازن [[React]] آنذاك القيمة <code>[5]</code> الناتجة من عملية التصيير السابقة مع القيمة <code>[5]</code> الناتجة من عملية التصيير التالية. لما كانت القيمتان في المصفوفة متساويتين (أي <code>5 === 5</code> محقق)، ستتخطى [[React]] التأثير. هذا الأمر مفيد جدًا في تحسين الأداء.
</syntaxhighlight>في المثال أعلاه، مرَّرنا <code>[count]</code> كوسيط ثانٍ. ولكن، ما الذي يعني تمرير مثل هذه القيمة؟ إن أصبحت قيمة المتغير <code>count</code> هي 5، وأُعيد تصيير المكون مع المتغير <code>count</code> الذي لا تزال قيمته ثابتة (أي 5)، فستوازن [[React]] آنذاك القيمة <code>[5]</code> الناتجة من عملية التصيير السابقة مع القيمة <code>[5]</code> الناتجة من عملية التصيير التالية. لمَّا كانت القيمتان في المصفوفة متساويتين (أي <code>5 === 5</code> محقق)، ستتخطى [[React]] التأثير. هذا الأمر مفيد جدًا في تحسين الأداء.


عندما تجرى عملية التصيير مع تغير القيمة <code>count</code> إلى 6، ستوازن [[React]] بين العناصر في المصفوفة <code>[5]</code> من عملية التصيير السابقة مع العناصر في المصفوفة <code>[6]</code> من عملية التصيير التالية؛ هذه المرة، ستعيد [[React]] تطبيق التأثير لأنَّ 5 لا تساوي 6 (أي <code>5 === 6</code> غير محقق). إن كان هنالك عدة عناصر (وليس عنصرًا واحدًا كما في حالتنا)، ستعيد [[React]] تنفيذ التأثير متى ما اختلفت قيمة أحد العناصر فقط.
عندما تجرى عملية التصيير مع تغير القيمة <code>count</code> إلى 6، ستوازن [[React]] بين العناصر في المصفوفة <code>[5]</code> من عملية التصيير السابقة مع العناصر في المصفوفة <code>[6]</code> من عملية التصيير التالية؛ هذه المرة، ستعيد [[React]] تطبيق التأثير لأنَّ 5 لا تساوي 6 (أي <code>5 === 6</code> غير محقق). إن كان هنالك عدة عناصر (وليس عنصرًا واحدًا كما في حالتنا)، ستعيد [[React]] تنفيذ التأثير متى ما اختلفت قيمة أحد العناصر فقط.
سطر 363: سطر 364:
هذا السلوك متاحٌ أيضًا من أجل التأثيرات التي تجري عمليات تنظيف:<syntaxhighlight lang="javascript">
هذا السلوك متاحٌ أيضًا من أجل التأثيرات التي تجري عمليات تنظيف:<syntaxhighlight lang="javascript">
useEffect(() => {
useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
   ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
   ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
   return () => {
   return () => {
سطر 372: سطر 377:
'''ملاحظة''': إن طبقت هذا السلوك من أجل تحسين الأداء، تأكد من أنَّ المصفوفة تتضمن أية قيمة من النطاق الخاجي تتغير عبر الزمن ويستعملها التأثير. إن لم يكن ذلك، فستشير شيفرتك إلى قيم قديمة ناتجة عن عمليات تصيير سابقة. سنناقش أيضًا خيارات أخرى لتحسين الأداء في الصفحة "[[React/hooks reference|مرجع إلى الواجهة البرمجية للخطافات]]".
'''ملاحظة''': إن طبقت هذا السلوك من أجل تحسين الأداء، تأكد من أنَّ المصفوفة تتضمن أية قيمة من النطاق الخاجي تتغير عبر الزمن ويستعملها التأثير. إن لم يكن ذلك، فستشير شيفرتك إلى قيم قديمة ناتجة عن عمليات تصيير سابقة. سنناقش أيضًا خيارات أخرى لتحسين الأداء في الصفحة "[[React/hooks reference|مرجع إلى الواجهة البرمجية للخطافات]]".


إن أردت تنفيذ تأثيرٍ ثمَّ تنظيفه مرةً واحدةً فقط (عند الوصل والفصل)، يمكن تمرير مصفوفة فارغة (أي <code>[]</code>) كمعاملٍ ثانٍ، إذ هذا يخبر [[React]] أنَّ التأثير لا يعتمد على أية قيم من الخاصيات (props) أو الحالة (state)؛ لذلك، فهو لا يحتاج إلى إعادة التنفيذ على الإطلاق. لا يعامل هذا الأمر على أنَّه حالة خاصة، إذ هذا تابعٌ لمدخلات المصفوفة مباشرةً وكيفية عملها. لما كانت عملية تمرير مصفوفة فارغة <code>[]</code> هي أقرب إلى النموذج الذهني (mental model) الشهير <code>componentDidMount</code> و <code>componentWillUnmount</code>، فإنَّنا ننصح بعدم اعتماد ذلك عادةً لأنها تؤدي غالبًا إلى حدوث أخطاء، كما [[React/hooks effect#.D9.84.D9.85.D8.A7.D8.B0.D8.A7 .D8.AA.D9.8F.D9.86.D9.81.D9.8E.D9.91.D8.B0.D8.AA .D8.A7.D9.84.D8.AA.D8.A3.D8.AB.D9.8A.D8.B1.D8.A7.D8.AA .D8.B9.D9.84.D9.89 .D9.83.D9.84 .D8.AA.D8.AD.D8.AF.D9.8A.D8.AB|ناقشنا ذلك مسبقًا]]. لا تنسَ أنَّ [[React]] تؤجل تنفيذ الخطاف <code>useEffect</code> لما بعد اكتمال المتصفح من عملية الرسم (paint)، لذا القيام بعمل إضافي أهون من المشكلة نفسها.
إن أردت تنفيذ تأثيرٍ ثمَّ تنظيفه مرةً واحدةً فقط (عند الوصل والفصل)، يمكن تمرير مصفوفة فارغة (أي <code>[]</code>) كمعاملٍ ثانٍ، إذ هذا يخبر [[React]] أنَّ التأثير لا يعتمد على أية قيم من الخاصيات (props) أو الحالة (state)؛ لذلك، فهو لا يحتاج إلى إعادة التنفيذ على الإطلاق. لا يعامل هذا الأمر على أنَّه حالة خاصة، إذ هذا تابعٌ لمدخلات المصفوفة مباشرةً وكيفية عملها. لما كانت عملية تمرير مصفوفة فارغة <code>[]</code> هي أقرب إلى النموذج الذهني (mental model) الشهير <code>componentDidMount</code> و <code>componentWillUnmount</code>، فإنَّنا ننصح بعدم اعتماد ذلك عادةً لأنَّها تؤدي غالبًا إلى حدوث أخطاء، كما [[React/hooks effect#.D9.84.D9.85.D8.A7.D8.B0.D8.A7 .D8.AA.D9.8F.D9.86.D9.81.D9.8E.D9.91.D8.B0.D8.AA .D8.A7.D9.84.D8.AA.D8.A3.D8.AB.D9.8A.D8.B1.D8.A7.D8.AA .D8.B9.D9.84.D9.89 .D9.83.D9.84 .D8.AA.D8.AD.D8.AF.D9.8A.D8.AB|ناقشنا ذلك مسبقًا]]. لا تنسَ أنَّ [[React]] تؤجل تنفيذ الخطاف <code>useEffect</code> لما بعد اكتمال المتصفح من عملية الرسم (paint)، لذا القيام بعمل إضافي أهون من المشكلة نفسها.


== الخطوات التالية ==
== الخطوات التالية ==
سطر 379: سطر 384:
أضف إلى ذلك أنَّنا بدأنا نرى كيف بإمكان الخطافات حل مشكلات ذُكرَت في القسم "[[React/hooks intro#.D8.A7.D9.84.D8.AD.D8.A7.D9.81.D8.B2 .D9.84.D8.A5.D8.B6.D8.A7.D9.81.D8.A9 .D8.A7.D9.84.D8.AE.D8.B7.D8.A7.D9.81.D8.A7.D8.AA|الحافز وراء إضافة الخطافات]]". وجدنا أيضًا كيف أن عملية تنظيف التأثير تلغي أية تكرارات في <code>componentDidUpdate</code> و <code>componentWillUnmount</code> وتجمع الشيفرة المترابطة وتجعلها قريبةً من بعضها بعضًا، وتساعد في تجنب حصول أخطاء. رأينا أيضًا كيف يمكننا فصل التأثيرات بحسب وظيفتها، وهذا الأمر لا يمكننا فعله في الأصناف مطلقًا.
أضف إلى ذلك أنَّنا بدأنا نرى كيف بإمكان الخطافات حل مشكلات ذُكرَت في القسم "[[React/hooks intro#.D8.A7.D9.84.D8.AD.D8.A7.D9.81.D8.B2 .D9.84.D8.A5.D8.B6.D8.A7.D9.81.D8.A9 .D8.A7.D9.84.D8.AE.D8.B7.D8.A7.D9.81.D8.A7.D8.AA|الحافز وراء إضافة الخطافات]]". وجدنا أيضًا كيف أن عملية تنظيف التأثير تلغي أية تكرارات في <code>componentDidUpdate</code> و <code>componentWillUnmount</code> وتجمع الشيفرة المترابطة وتجعلها قريبةً من بعضها بعضًا، وتساعد في تجنب حصول أخطاء. رأينا أيضًا كيف يمكننا فصل التأثيرات بحسب وظيفتها، وهذا الأمر لا يمكننا فعله في الأصناف مطلقًا.


إلى هذا الحد، قد تتساءل عن كيفية عمل الخطافات، وكيف تستطيع [[React]] أن تعرف أي استدعاء للخطاف <code>useState</code> يقابل أي متغير حالة بين عمليات التصيير، وكيف تطابق [[React]] التأثيرات السابقة واللاحقة في كل تحديث. في الصفحة التالية سنتعلم حول [[React/hooks rules|قواعد الخطافات]]، إذ هي ضرورية لجعل الخطافات تعمل بشكل صحيح.
قد تتساءل الآن عن كيفية عمل الخطافات، وكيف تستطيع [[React]] أن تعرف أي استدعاء للخطاف <code>useState</code> يقابل أي متغير حالة بين عمليات التصيير، وكيف تطابق [[React]] التأثيرات السابقة واللاحقة في كل تحديث. في الصفحة التالية سنتعلم حول [[React/hooks rules|قواعد الخطافات]]، إذ هي ضرورية لجعل الخطافات تعمل بشكل صحيح.


==انظر أيضًا==
==انظر أيضًا==

المراجعة الحالية بتاريخ 11:11، 6 نوفمبر 2020

الخطافات هي إضافة جديدة إلى الإصدار 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 (أي JavaScript closures) وتتجنب تعريف واجهات React برمجية مخصصة، إذ وفرت JavaScript حلًا مسبقًا.

هل يُنفَّذ الخطاف 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. هذا يحدث من أجل كل عملية تصيير بما فيها عملية التصيير الأولى.

قد يلاحظ مطورو JavaScript المتمرسون أنَّ الدالة التي مررناها إلى الخطاف 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(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

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

تسمح الخطافات بفصل الشيفرة بناءً على وظيفتها بدلًا من اسم تابع دورة الحياة. ستطبق 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);     // تنفيذ أول تأثير

// { friend: { id: 200 } } التحديث مع الخاصيات
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // تنظيف التأثير السابق
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // تنفيذ التأثير التالي

// { friend: { id: 300 } } التحديث مع الخاصيات
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // تنظيف التأثير السابق
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // تنفيذ التأثير التالي

// الفصل
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // تنظيف التأثير الأخير

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

تحسين الأداء عبر تخطي التأثيرات

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

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

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

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // فقط count أعد تنفيذ التأثير إن تغير

في المثال أعلاه، مرَّرنا [count] كوسيط ثانٍ. ولكن، ما الذي يعني تمرير مثل هذه القيمة؟ إن أصبحت قيمة المتغير count هي 5، وأُعيد تصيير المكون مع المتغير count الذي لا تزال قيمته ثابتة (أي 5)، فستوازن React آنذاك القيمة [5] الناتجة من عملية التصيير السابقة مع القيمة [5] الناتجة من عملية التصيير التالية. لمَّا كانت القيمتان في المصفوفة متساويتين (أي 5 === 5 محقق)، ستتخطى React التأثير. هذا الأمر مفيد جدًا في تحسين الأداء.

عندما تجرى عملية التصيير مع تغير القيمة count إلى 6، ستوازن React بين العناصر في المصفوفة [5] من عملية التصيير السابقة مع العناصر في المصفوفة [6] من عملية التصيير التالية؛ هذه المرة، ستعيد React تطبيق التأثير لأنَّ 5 لا تساوي 6 (أي 5 === 6 غير محقق). إن كان هنالك عدة عناصر (وليس عنصرًا واحدًا كما في حالتنا)، ستعيد React تنفيذ التأثير متى ما اختلفت قيمة أحد العناصر فقط.

هذا السلوك متاحٌ أيضًا من أجل التأثيرات التي تجري عمليات تنظيف:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // فقط props.friend.id أعد الاشتراك إذا تغير

مستقبلًا، قد يضاف هذا الوسيط الثاني تلقائيًّا عبر تحول مبني على الوقت (build-time transformation).

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

إن أردت تنفيذ تأثيرٍ ثمَّ تنظيفه مرةً واحدةً فقط (عند الوصل والفصل)، يمكن تمرير مصفوفة فارغة (أي []) كمعاملٍ ثانٍ، إذ هذا يخبر React أنَّ التأثير لا يعتمد على أية قيم من الخاصيات (props) أو الحالة (state)؛ لذلك، فهو لا يحتاج إلى إعادة التنفيذ على الإطلاق. لا يعامل هذا الأمر على أنَّه حالة خاصة، إذ هذا تابعٌ لمدخلات المصفوفة مباشرةً وكيفية عملها. لما كانت عملية تمرير مصفوفة فارغة [] هي أقرب إلى النموذج الذهني (mental model) الشهير componentDidMount و componentWillUnmount، فإنَّنا ننصح بعدم اعتماد ذلك عادةً لأنَّها تؤدي غالبًا إلى حدوث أخطاء، كما ناقشنا ذلك مسبقًا. لا تنسَ أنَّ React تؤجل تنفيذ الخطاف useEffect لما بعد اكتمال المتصفح من عملية الرسم (paint)، لذا القيام بعمل إضافي أهون من المشكلة نفسها.

الخطوات التالية

مبارك لك! كانت هذه الصفحة طويلة ولكن تحمل في طيَّاتها أجوبةً على أسئلتك. تعملت إلى الآن خطاف الحالة وخطاف التأثير، والآن يمكنك فعل الكثير من الأشياء باستعمالهما سويةً، إذ يشملان أغلب حالات استعمال الأصناف؛ في الحالات التي لا تجد فيهما ضالتك، يمكن أن تبحث في صفحة الخطافات الإضافية فربما تجدها هنالك.

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

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

انظر أيضًا

 مصادر