درود دوستان 👋 دو تکنیک پرکاربرد بهینه‌سازی توی ری‌اکت Throttle و Debounce هستن که هر دو پیاده‌سازی نسبتاً شبیه به هم دارن و برای پیاده‌سازی قابلیتی به اسم Rate Limiting به کار میرن، اما در اصل برای هدف متفاوتی به کار میرن. توی این قسمت می‌خوایم Throttle رو توی ری‌اکت پیاده‌سازی کنیم. این پست از مجموعه پست‌های ری‌اکت ۱۰۱ هست که شامل مجموعه‌ای از پست‌های کوتاه و کاربردی ری‌اکتی میشه.

البته قبلاً یک پست نوشتم درباره پیاده‌سازی این تکنیک‌ها توی جاوااسکریپت:

 

Throttle چیه؟ 🤔

Throttle تضمین می‌کنه که کدهای ما توی یک بازه زمانی مشخص فقط یک بار اجرا بشن. مثلاً یک دکمه داریم که وقتی روی اون کلیک میشه تابعی رو اجرا می‌کنه. اما می‌خوایم کاری کنیم که کاربر توی هر ۱۰ ثانیه فقط یک بار بتونه اون رو اجرا کنه. بنابراین اگه کاربر توی بازه ۱۰ ثانیه، صد بار هم رو دکمه کلیک کنه، Throttle کمک می‌کنه تابع فقط یک بار اجرا بشه، و تابع برای بار دوم توی ۱۰ ثانیه بعدی قابل اجرا هست.

نکتهٔ جالبی که می‌خوام براتون به اشتراک بذارم، نحوهٔ کارایی قسمت «واکنش شما به این پست؟» هست که انتهای هر پست توی دیتی دیده میشه. این قسمت با استفاده از مخلوطی از دو تکنیک Throttle و Debounce پیاده‌سازی شده و کاربر می‌تونه توی بازه زمانی مشخص (۰.۵ ثانیه) فقط یک واکنش نشون بده (بخش Throttle ماجرا). و همهٔ واکنش‌های کاربر ابتدا توی فرانت‌اند ذخیره میشه و نهایتاً زمانی به سرور ارسال میشه که بازه زمانی مشخصی از آخرین واکنش گذشته باشه (بخش Debounce ماجرا.)

قسمت جالب چیزی که براتون مطرح کردم و دوست دارم شفاف‌سازی کنم اینه که خیلی از کاربران به من پیام دادن که این بخش باگ داره و کاربر می‌تونه بیشتر از یک واکنش نشون بده. اما این بخش به همین شکل پیاده‌سازی شده که شما بتونین چندین واکنش نشون بدین، و به قول معروف «باگ نیست، فیچره 😄» 

بریم که این تکنیک رو توی ری‌اکت پیاده‌سازی کنیم.

 

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

این تکنیک و همچنین Debounce رو با نوشتن یک هوک پیاده‌سازی خواهیم کرد، چون راحت‌تره. اسم این هوک رو می‌ذاریم useThrottle که به این صورت قراره از اون توی کامپوننت‌ها استفاده کنیم:

import { useThrottle } from "./useThrottle";

export default function App() {
  const throttle = <<useThrottle(1000);>>

  const myCallback = <<throttle>>(() => {
    // api call ...
  });

  return <button onClick={myCallback}>Check</button>;
}

این کامپوننت App.tsx هست که این هوک توی اون داره استفاده میشه (این کامپوننت رو بخاطر بسپارین.) توی خط ۴ ما این هوک رو پیاده‌سازی کردیم و عدد 1000 رو بهش پاس دادیم، به این معنی که می‌خوایم کال‌بک مد نظر ما (خط ۶) توی بازهٔ زمانی 1000 میلی‌ثانیه (۱ ثانیه) فقط یک بار اجرا بشه. خب بریم که این هوک رو پیاده‌سازی کنیم.

برای این کار ابتدا یک فایل می‌سازیم به اسم useThrottle.ts و توی اون یک هوک خالی رو پیاده‌سازی می‌کنیم:

// useThrottle.ts

export function useThrottle() {

}

توی این هوک می‌بایست یک تابع درست کنیم که قراره ریترن بشه و مورد استفاده قرار بگیره:

export function useThrottle() {
  const throttle = () => {
    // ...
  }

  return throttle;
}

همونطور که توی کامپوننت App.tsx دیدیم، تابع throttle یک کال‌بک قبول کرده. پس اون رو به این شکل تغییر می‌دیم:

export function useThrottle() {
  const throttle = (<<callback>>) => {
    return (...args) => callback(...args);
  }

  return throttle;
}

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

تا الان هنوز کدی برای عملیات به تأخیر انداختن ننوشتیم. باید یک سری شرط و شروط رو بررسی کنیم. ابتدا یک ref می‌سازیم:

import { useRef } from "react";

export function useThrottle() {
  const lastCall = useRef(0);

  const throttle = (callback) => {
    // ...
  }

  return throttle;
}

توی خط ۴ یک ref ساختیم که مسئول نگهداری زمان آخرین اجرای کال‌بک هست.

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

توی App.tsx دیدیم که هوک ما یک ورودی می‌گرفت برای مشخص کردن بازهٔ زمانی مد نظر. پس اون رو پیاده‌سازی می‌کنیم:

import { useRef } from "react";

export function useThrottle(<<duration>>) {
  // ...
}

حالا باید یک مقایسه انجام بدیم. باید مقایسه کنیم که آیا از آخرین زمانی که کال‌بک اجرا میشه، زمانی به اندازهٔ duration (که توی مثال ما که ۱۰۰۰ میلی‌ثانیه تعیین کردیم) گذشته یا نه. این مقایسه رو توی خط ۸ نوشتیم:

import { useRef } from "react";

export function useThrottle(duration) {
  const lastCall = useRef(0);

  const throttle = (callback) => {
    return (...args) => {
      if (lastCall.current + duration < Date.now()) {
        lastCall.current = Date.now();
        callback(...args);
      }
    };
  }

  return throttle;
}

اگه زمانی به اندازهٔ duration گذشته باشه، شرط برقرار میشه و کال‌بک اجرا میشه. همچنین می‌بایست آخرین زمان اجرا رو بروز کنیم که این کار رو توی خط ۹ انجام دادیم.

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

import { <<useCallback>>, useRef } from "react";

export function useThrottle(duration) {
  const lastCall = useRef(0);

  const throttle = <<useCallback>>(
    (callback) => {
      return (...args) => {
        if (lastCall.current + duration < Date.now()) {
          lastCall.current = Date.now();
          callback(...args);
        }
      };
    },
    [duration]
  );

  return throttle;
}

خب دیدیم که چقدر راحت تونستیم این هوک رو پیاده‌سازی کنیم. نسخهٔ دمو رو می‌تونید ببنید.

 

امیدوارم از نکاتی که بررسی کردیم استفاده کرده باشین. توی قسمت بعدی تکنیک Debounce رو پیاده‌سازی می‌کنیم. روزتون خوش 😉🌹