استخدام Debounce و Throttle في React وتحويلهما إلى Hooks مخصصة

دقائق القراءة: 6

تُعد Hooks إضافة رائعة إلى مكتبة React، فقد ساهمت بشكل كبير في تبسيط العديد من الأنماط المنطقية التي كانت تتطلب في السابق تقسيمها إلى دورات حياة مختلفة عند استخدام class components. ومع ذلك، فإنها تتطلب نموذجًا ذهنيًا مختلفًا، خاصة للمطورين الجدد.

فهم Debounce و Throttle

هناك العديد من المقالات التي تتناول مفهومي debounce و throttle بالتفصيل، ولذلك لن نتعمق في كيفية كتابة هذه الدوال من الصفر. لغرض الإيجاز، سنعتمد على تطبيقيهما المتوفرين في مكتبة Lodash. إذا كنت بحاجة إلى تذكير سريع، فإن كلتا الدالتين تقبلان دالة استدعاء عكسي (callback function) وتأخيرًا بالمللي ثانية (لنقل x)، ثم تعيدان دالة أخرى بسلوك خاص:

  • debounce: تُعيد دالة يمكن استدعاؤها أي عدد من المرات (ربما بتعاقب سريع)، ولكنها لن تستدعي الدالة الأصلية (callback) إلا بعد انتظار x مللي ثانية من آخر استدعاء لها. هذا مفيد للحالات التي تريد فيها تنفيذ إجراء ما بعد توقف المستخدم عن فعل شيء لفترة معينة، مثل التوقف عن الكتابة.
  • throttle: تُعيد دالة يمكن استدعاؤها أي عدد من المرات (ربما بتعاقب سريع)، ولكنها لن تستدعي الدالة الأصلية (callback) أكثر من مرة واحدة كل x مللي ثانية. هذا مفيد للحالات التي تريد فيها تقييد عدد مرات تنفيذ إجراء معين خلال فترة زمنية محددة، مثل معالجة أحداث التمرير (scroll events) أو تغيير حجم النافذة (resize events).

سيناريو عملي: محرر مدونة بسيط

لنفترض أن لدينا محرر مدونة بسيط ونرغب في حفظ محتوى المقال في قاعدة البيانات بعد ثانية واحدة من توقف المستخدم عن الكتابة. هذا هو السيناريو المثالي لتطبيق debounce. لنلقِ نظرة على نسخة مبسطة من محررنا:

المحرر الأساسي في React

 import React, { useState } from 'react' ;
 import debounce from 'lodash.debounce' ;
 
 function App ( ) {
   const [value, setValue] = useState( '' );
   const [dbValue, saveToDb] = useState( '' ); // would be an API call normally
 
   const handleChange = event => {
     setValue(event.target.value);
   };
 
   return (
     < main >
       < h1 > Blog </ h1 >
       < textarea value = {value} onChange = {handleChange} rows = {5} cols = {50} />
       < section className = "panels" >
         < div >
           < h2 > Editor (Client) </ h2 >
           {value}
         </ div >
         < div >
           < h2 > Saved (DB) </ h2 >
           {dbValue}
         </ div >
       </ section >
     </ main >
   );
 }

في هذا المثال، تمثل الدالة saveToDb عادةً استدعاءً لواجهة برمجية (API call) إلى الواجهة الخلفية (backend). لتبسيط الأمور، نقوم بحفظ القيمة في حالة (state) وعرضها كـ dbValue. بما أننا نريد تنفيذ عملية الحفظ هذه فقط بعد توقف المستخدم عن الكتابة (لمدة ثانية واحدة)، يجب أن تكون هذه العملية debounced.

إنشاء دالة Debounced

أولاً، نحتاج إلى دالة debounced تغلف استدعاء saveToDb:

تطبيق Debounce الأولي (غير صحيح)

 import React, { useState } from 'react' ;
 import debounce from 'lodash.debounce' ;
 
 function App ( ) {
   const [value, setValue] = useState( '' );
   const [dbValue, saveToDb] = useState( '' ); // would be an API call normally
 
   const handleChange = event => {
     const { value : nextValue } = event.target;
     setValue(nextValue);
     // highlight-starts
     const debouncedSave = debounce( () => saveToDb(nextValue), 1000 );
     debouncedSave();
     // highlight-ends
   };
 
   return < main > {/* Same as before */} </ main > ;
 }

لكن هذا التنفيذ لا يعمل كما هو متوقع، لأن الدالة debouncedSave يتم إنشاؤها حديثًا في كل مرة يتم فيها استدعاء handleChange. هذا سيؤدي إلى تطبيق debounce على كل ضغطة مفتاح بدلاً من تطبيقه على قيمة الإدخال بأكملها بعد توقف الكتابة.

الحل باستخدام useCallback

يُستخدم useCallback عادةً لتحسين الأداء عند تمرير دوال الاستدعاء العكسي (callbacks) إلى المكونات الفرعية (child components). ولكن يمكننا الاستفادة من ميزته في تخزين دالة الاستدعاء العكسي (memoizing a callback function) لضمان أن debouncedSave يشير إلى نفس الدالة debounced عبر عمليات إعادة التصيير (renders). هذا هو الحل الصحيح:

تطبيق Debounce مع useCallback

 import React, { useState, useCallback } from 'react' ;
 import debounce from 'lodash.debounce' ;
 
 function App ( ) {
   const [value, setValue] = useState( '' );
   const [dbValue, saveToDb] = useState( '' ); // would be an API call normally
 
   // highlight-starts
   const debouncedSave = useCallback(
     debounce( nextValue => saveToDb(nextValue), 1000 ),
     [], // will be created only once initially
   );
   // highlight-ends
 
   const handleChange = event => {
     const { value : nextValue } = event.target;
     setValue(nextValue);
     // Even though handleChange is created on each render and executed
     // it references the same debouncedSave that was created initially
     debouncedSave(nextValue);
   };
 
   return < main > {/* Same as before */} </ main > ;
 }

في هذا التنفيذ، على الرغم من أن الدالة handleChange يتم إنشاؤها وتنفيذها في كل عملية إعادة تصيير، إلا أنها تشير إلى نفس الدالة debouncedSave التي تم إنشاؤها في البداية بفضل useCallback.

الحل البديل باستخدام useRef

يوفر لنا useRef كائنًا قابلاً للتعديل (mutable object) تشير خاصيته current إلى القيمة الأولية التي تم تمريرها. إذا لم نقم بتغييرها يدويًا، فستستمر القيمة طوال دورة حياة المكون (component's lifetime). هذا مشابه لخصائص مثيل الفئة (class instance properties) في المكونات القائمة على الفئات (class-based components). هذا الحل يعمل أيضًا كما هو متوقع:

تطبيق Debounce مع useRef

 import React, { useState, useRef } from 'react' ;
 import debounce from 'lodash.debounce' ;
 
 function App ( ) {
   const [value, setValue] = useState( '' );
   const [dbValue, saveToDb] = useState( '' ); // would be an API call normally
 
   // This remains same across renders
   // highlight-starts
   const debouncedSave = useRef(debounce( nextValue => saveToDb(nextValue), 1000 ))
     .current;
   // highlight-ends
 
   const handleChange = event => {
     const { value : nextValue } = event.target;
     setValue(nextValue);
     // Even though handleChange is created on each render and executed
     // it references the same debouncedSave that was created initially
     debouncedSave(nextValue);
   };
 
   return < main > {/* Same as before */} </ main > ;
 }

هنا، يتم تخزين الدالة debouncedSave في مرجع useRef، مما يضمن بقاءها ثابتة عبر عمليات إعادة التصيير، وبالتالي حل مشكلة إعادة إنشاء الدالة في كل مرة.

الخلاصة التقنية

يُعد استخدام debounce و throttle من التقنيات الأساسية لتحسين أداء تطبيقات React وتجربة المستخدم، خاصة عند التعامل مع الأحداث المتكررة مثل إدخال النصوص أو التمرير. لقد رأينا كيف أن مجرد تطبيق debounce بشكل مباشر قد يؤدي إلى سلوك غير متوقع بسبب إعادة إنشاء الدوال في كل عملية تصيير. لحل هذه المشكلة، قدمت React Hooks حلولاً أنيقة باستخدام useCallback أو useRef لضمان استمرارية الدوال عبر دورات حياة المكونات. يتيح لنا ذلك إنشاء دوال debounced أو throttled تعمل بكفاءة وفعالية، مما يقلل من الحمل على الواجهة الخلفية ويحسن استجابة الواجهة الأمامية. فهم هذه الفروقات الدقيقة وكيفية تطبيقها بشكل صحيح أمر بالغ الأهمية لأي مطور React يسعى لبناء تطبيقات عالية الأداء.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *