درود دوستان 👋 توی این قسمت می‌خوایم یاد بگیریم که چطوری یکی از پرکاربردترین تکنیک‌های بهینه‌سازی و افزایش سرعت و تجربهٔ کاربری برنامه‌های ری‌اکتی رو پیاده‌سازی کنیم. اسم این تکنیک Debounce هست. این پست از ...

 

Debounce چیه؟ 🤔

Debounce یک تکنیک بهینه‌سازی توی دنیای برنامه‌نویسی هست و فقط مختص ری‌اکت نیست. با اون می‌تونیم مطمئن بشیم که یک قطعه کد دوباره اجرا نمیشه مگر اینکه مقدار مشخصی از زمان آخرین تلاش برای اجرای اون گذشته باشه. معروف‌ترین مثال Debounce برای پیاده‌سازی جستجوی لحظه‌ای هست. ابتدا صبر می‌کنیم تا کاربر دست از تایپ کردن برداره و بعد جستجو رو شروع می‌کنیم. مثلاً می‌گیم اگه از آخرین تایپ کاربر ۱ ثانیه گذشته بود جستجو رو شروع کن. در غیر این صورت، جستجو رو به تعویق بنداز. تابع setTimeout معمولاً برای پیاده‌سازی چنین توابعی به کار میره.

این تکنیک رو قبلاً با جاوااسکریپت پیاده‌سازی کردیم. اما می‌خوایم ببینیم چطوری با قابلیت‌های ری‌اکت مثل هوک‌ها اون رو پیاده‌سازی کنیم.

 

پیاده‌سازی Debounce توی ری‌اکت

خب برای این کار همهٔ چیزی که باید انجام بدیم اینه که یک هوک بسازیم. یک هوک به اسم useDebounce که قراره به شکل زیر توی برنامه استفاده بشه:

function MyComponent() {
  const debounce = <<useDebounce(1000);>>

  const handleSearch = <<debounce(() => {>>
    // Executes after 1000ms
  });

  return (
    <input onChange={handleSearch} /> 
  );
}

کال‌بک مد نظرمون (همون کدهایی که قراره Debounce بشه) رو محصور می‌کنیم به تابعی که از هوک useDebounce ریترن میشه. هنگام پیاده‌سازی این هوک هم می‌بایست بازه زمانی مد نظرمون رو هم مشخص کنیم که اینجا گفتیم ۱۰۰۰ میلی‌ثانیه، یعنی می‌بایست از آخرین اجرای کدهای ما ۱۰۰۰ میلی‌ثانیه گذشته باشه تا بتونیم مجدد اونها رو اجرا کنیم. بریم که این هوک رو پیاده‌سازی کنیم.

ابتدا یک فایل می‌سازیم به اسم useDebounce.js و توی اون هوک رو پیاده‌سازی می‌کنیم:

export function useDebounce(delay) {
  const debounce = () => {
    // ...
  }

  return debounce;
}

اینجا یک هوک ساختیم که داره یک تابع به اسم debounce رو ریترن می‌کنه. کاری که الان باید انجام بدیم تکمیل کردن این تابع هست. این تابع می‌بایست یک HOF باشه. یعنی باید یک تابع دیگه رو قبول کنه و حالت بهینه‌سازی شدهٔ اون رو برگردونه:

export function useDebounce(delay) {
  const debounce = (callback) => {
    <<return (...args) => {>>
      // ...
      callback(...args);
    }
  }

  return debounce;
}

حالا باید تکنیک debounce رو پیاده‌سازی کنیم که توی جاوااسکریپت با استفاده از setTimeout معمولاً انجام میشه. با استفاده از این تابع می‌تونیم عملیات مد نظرمون رو به تأخیر بندازیم؛ مثل خط ۴ تا ۶ کد زیر:

export function useDebounce(delay) {
  const debounce = (callback) => {
    return (...args) => {
      <<setTimeout(() => {>>
        callback(...args);
      }, <<delay>>)
    }
  }

  return debounce;
}

Debounce ما هنوز کامل نیست. هر بار که کد بالا اجرا میشه یک setTimeout جدید ساخته میشه و در نتیجهٔ اون setTimeout اولی هم غیر قابل دسترس میشه. در صورتی که ما باید فقط یک setTimeout داشته باشیم و بتونیم اون رو مدیریت کنیم (یعنی ابتدا باید اون رو از بین ببریم تا بتونیم یکی دیگه بسازیم.) این کار رو با استفاده از useRef ری‌اکت انجام می‌دیم:

export function useDebounce(delay) {
  const timeoutId = <<useRef();>>

  const debounce = (callback) => {
    return (...args) => {
      <<timeoutId.current>> = setTimeout(() => {
        callback(...args);
      }, delay);
    }
  }

  return debounce;
}

توی خط ۲ کد بالا یک Ref ساختیم توی خط ۶ به اون مقدار دادیم. هر تابع setTimeout یک شناسه منحصر به فرد داره. ما برای از بین بردن اون به این شناسه احتیاج داریم. برای همین از useRef استفاده کردیم و مقدار اون رو برابر با شناسهٔ setTimeout قرار دادیم.

دلیل استفاده از Ref هم اینه که می‌خوایم مقدارش توی رندرها ثابت باقی بمونه. البته می‌شد از useState هم استفاده کرد، اما useRef برای چنین شرایطی مناسب‌تر به نظر میاد. چون مقدار مد نظر ما Reactive نیست و نمی‌خوایم وقتی تغییر کرد کامپوننت مجدد رندر بشه. و این کار فقط از useRef بر میاد.

حالا هر بار می‌خواد یک setTimeout جدید ساخته بشه، بعد می‌بایست کاری کنیم که setTimeout قبلی از بین بره، که این کار رو توی خط ۶ انجام دادیم:

export function useDebounce(delay) {
  const timeoutId = useRef();

  const debounce = (callback) => {
    return (...args) => {
      <<clearTimeout(timeoutId.current); >>
      timeoutId.current = setTimeout(() => {
        callback(...args);
      }, delay);
    }
  }

  return debounce;
}

خب اینجا کار ساختن این هوک تقریباً تمومه و می‌تونیم خیلی راحت به همون شکلی قبل‌تر بررسی کردیم توی برنامه ازش استفاده کنیم. اما بهتره چند تا نکتهٔ بهینه‌سازی رو رعایت کنیم. ابتدا محصور کردن تابع debounce درون یک useCallback هست تا از رندرهای ناخواسته جلوگیری کنیم:

export function useDebounce(delay) {
  const timeoutId = useRef();

  const debounce = <<useCallback(>>
    (callback) => {
      return (...args) => {
        clearTimeout(timeoutId.current);

        timeoutId.current = setTimeout(() => {
          callback(...args);
        }, delay);
      };
    },
    [delay]
  );

  return debounce;
}

مرحلهٔ دوم اینه که کاری کنیم وقتی کامپوننت یا هوک Unmount میشه، setTimeout مد نظرمون رو هم پاک کنیم تا Memory Leak نداشته باشیم. این کار رو با استفاده از useEffect انجام می‌دیم:

export function useDebounce(delay) {
  const timeoutId = useRef();

  const debounce = useCallback(
    (callback) => {
      return (...args) => {
        clearTimeout(timeoutId.current);

        timeoutId.current = setTimeout(() => {
          callback(...args);
        }, delay);
      };
    },
    [delay]
  );

  <<useEffect(() => {>>
    return () => {
      if (timeoutId.current) {
        clearTimeout(timeoutId.current);
      }
    };
  }, []);

  return debounce;
}

خب هوک بالا آماده هست که توی برنامه مورد استفاده قرار بگیره. یک نسخه دمو از هوک بالا ساختم که اون رو می‌تونین از ببینید.

 

امیدوارم از نکاتی که بررسی کردیم استفاده کرده باشین. تا قسمت بعدی خدانگهدار 😉👋