React هوک‌های مختلفی را ارائه می‌دهد که مدیریت state برنامه و سایر ویژگی‌های React را در کامپوننت‌های فانکشنال آسان‌تر می‌کند. هوک‌ها ویژگی‌های کلاس کامپوننت‌ها را برای کامپوننت‌های فانکشنال فراهم می‌کنند و در مقایسه با کلاس کامپوننت‌ها به کدنویسی کم‌تری نیاز دارند. در این بین، ما useMemo و useCallback را داریم که درک تفاوت بین این دو هوک و به کار گرفتن آن‌ها به بهبود عملکرد برنامه‌های ما کمک می‌کند.

در این مقاله قصد داریم تا در مورد هر دو هوک useMemo و useCallback بحث کنیم و با تفاوت بین آن‌ها و زمان استفاده از هر هوک بیشتر آشنا شویم.

هوک useMemo

هوک useMemo مقدار بازگشتی یک محاسبه پرهزینه بین رندرها را به خاطر می‌سپارد. Memoizing به معنای ذخیره value به عنوان یک مقدار ذخیره‌شده در حافظه cache است تا نیازی به محاسبه مجدد نباشد، مگر اینکه لازم باشد این کار را مجددا انجام دهیم.

useMemo هوکی است که برای بهینه‌سازی عملکرد رندرهای ما مورد استفاده قرار می‌گیرد. به طور معمول، زمانی که یک متغیر را در داخل یک کامپوننت تعریف می‌کنیم در هر بار رندر شدن، آن متغیر دوباره ایجاد می‌شود. اگر این متغیر مقدار بازگشتی یک تابع را ذخیره کند، هر بار که کامپوننت ما رندر می‌شود، تابع فراخوانی می‌گردد.

معمولا این موضوع باعث ایجاد مشکل نمی‌شود. اما، اگر تابعی که داریم یک تابع پرهزینه باشد چه اتفاقی می‌افتد؟ به عنوان مثال:

function calculate() {
  let result = 0;
  for (let i = 0; i < 1000000000; i++) {
    result += i;
  }
  return result;
}

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

  const value = calculate();

  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <p>Count: {count}</p>
    </div>
  );
}

هنگامی که روی Increment Count کلیک می‌کنیم، چند ثانیه طول می‌کشد تا state شمارش به‌روزرسانی شود. این اتفاق به این دلیل است هر بار که یک کامپوننت پس از ایجاد تغییری در state مجددا رندر می‌شود، تابع calculate نیز دوباره اجرا می‌گردد.

حال تصور کنید که چندین متغیر state در کامپوننت خود داشته باشیم که هر کدام هدف خاص خود را دنبال می‌کنند و هر به‌روزرسانی state، باعث رندر مجدد و اجرای این تابع پرهزینه می‌شود.

این متغیرهای state ممکن است کاملاً با محاسبه پرهزینه انجام شده نامرتبط باشند که باعث تاخیرهای غیرضروری می‌شود. همین موضوع بر روی عملکرد وب‌سایت ما تأثیر منفی می‌گذارد و می‌تواند منجر به ایجاد تجربه کاربری نامناسب شود.

سینتکس هوک useMemo

هوک useMemo می‌تواند به ما در حل این مشکل کمک کند. ابتدا سینتکس آن را بررسی می‌کنیم:

const value = useMemo(expensiveFunction, [...dependencyArray])

ما باید این هوک را در قسمت بالای کامپوننت خود تعریف کنیم. آرگومان‌هایی که هوک useMemo دریافت می‌کند به شرح زیر می‌باشد:

در اولین رندر، هوک useMemo نتیجه‌ی expensiveFunction را return کرده و آن را ذخیره می‌کند. در طول رندرهای بعدی، اگر هیچ یک از dependencyها تغییر نکرده باشد، useMemo مقدار ذخیره شده را return می‌کند. اما اگر هر کدام از این dependencyها تغییر کنند، تابع را مجددا فراخوانی کرده و اجرا می‌نماید.

بررسی هوک useMemo در یک مثال

اکنون از هوک useMemo در مثالی که داریم استفاده می‌کنیم:

const [dependentCount, setDependentCount] = useState(10);

const value = useMemo(calculate, [dependentCount]);

return (
  <div className="App">
   	
    // ...

    <button onClick={() => setDependentCount(dependentCount + 1)}>
      Increment Dependent Count
    </button>
    <p>Dependent Count: {dependentCount}</p>
  </div>
);

ما یک state dependentCount دیگر ایجاد کرده‌ایم که فرض می‌کنیم وابسته به محاسبه پرهزینه‌ای که داریم، می‌باشد. زمانی که این state کامپوننت را به‌روزرسانی و رندر می‌کند، تابع calculate اجرا می‌شود. اما اگر هر state دیگری تغییر کند، useMemo به جای اجرای مجدد تابع، مقدار ذخیره شده را return می‌کند.

اکنون، هنگامی که روی Increment Count کلیک می‌کنیم، رندر سریع‌تر انجام می‌شود. زیرا تابع calculate در هر رندر فراخوانی نمی‌گردد. این اتفاق در طول هر به‌روزرسانی state دیگری که در آرایه dependency هوک useMemo فهرست نشده است، یکسان می‌باشد.

اما هنگامی که روی Increment dependent Count کلیک می‌کنیم، مدت زمانی طول می‌کشد تا مقدار به‌روزرسانی شده رندر شود. این به این دلیل است که dependentCount یک dependency مربوط به useMemo است و تغییر آن، تابع پرهزینه را فراخوانی می‌کند؛ بنابراین زمان می‌برد تا کامپوننت مجددا رندر شود.

به این ترتیب، با استفاده از هوک useMemo، می‌توانیم اجرای یک تابع پرهزینه را با فراخوانی آن فقط برای به‌روزرسانی‌های stateای که در واقع به مقدار بازگشتی آن نیاز دارند، کنترل کنیم. همین موضوع می‌تواند عملکرد برنامه ما را به شدت بهبود بخشد.

چه زمانی باید از هوک useMemo استفاده کنیم؟

هوک useCallback

هوک useCallback یک تابع callback را به خاطر می‌سپارد و آن را return می‌کند.

باید به این نکته توجه داشته باشیم که useCallback خود تابع را حفظ می‌کند، نه مقدار بازگشتی آن را. همچنین این هوک تعریف تابع یا رفرنس تابع را نیز در حافظه cache ذخیره می‌کند. اما، هوک useMemo مقدار بازگشتی توابع را در حافظه cache ذخیره می‌کند تا دیگر نیازی به اجرای تابع نباشد.

تابعی که در داخل یک کامپوننت تعریف شده است، در هر رندر کامپوننت، شبیه به یک متغیر، دوباره ایجاد می‌شود. اما تفاوت این است که هر بار با یک رفرنس متفاوت رندر می‌شود. بنابراین، یک useEffect وابسته به این تابع در هر رندر دوباره اجرا می‌گردد. مشابه این اتفاق در مورد کامپوننت‌های child نیز اتفاق می‌افتد.

به عنوان مثال:

const App = () => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");

  const handleClick = () => {
    setValue("Kunal");
  };
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <p>Count: {count}</p>
      <p>Value: {value}</p>
      <SlowComponent handleClick={handleClick} />
    </div>
  );
};

const SlowComponent = React.memo(({ handleClick, value }) => {
  
  // Intentially making the component slow
  for (let i = 0; i < 1000000000; i++) {}

  return (
    <div>
      <h1>Slow Component</h1>
      <button onClick={handleClick}>Click Me</button>
      
    </div>
  );
});

در این مثال، ما یک SlowComponent به عنوان کامپوننت child برای کامپوننت App داریم. هنگامی که یک کامپوننت parent رندر می‌شود، همه کامپوننت‌های child آن رندر می‌شوند، صرف نظر از اینکه آیا چیزی در آن‌ها تغییر کرده است یا خیر.

برای جلوگیری از رندرهای غیرضروری کامپوننت‌های child، ما معمولاً از تابع React.memo استفاده می‌کنیم. این تابع اساساً کامپوننت را cache می‌کند و تنها در صورتی آن را دوباره رندر می‌کند که ویژگی‌های آن تغییر کرده باشد.

اکنون، هنگامی که روی Increment Count کلیک می‌کنیم، باز هم زمان زیادی طول می‌کشد تا رندر شود، زیرا SlowComponent در تغییر state، دوباره رندر می‌شود. اما چرا اینطور است؟ ما هیچ یک از props آن را تغییر نمی‌دهیم.

در ظاهر، ممکن است اینطور به نظر برسد که ما مقدار prop handleClick را تغییر نمی‌دهیم. اما از آنجایی که توابع با یک رفرنس متفاوت دوباره ایجاد می‌شوند، در هر رندر کامپوننت App، کامپوننت child آن (که در این مثال SlowComponent است) رندر می‌شود.

برای حفظ برابری ارجاعی، ما تعریف این تابع را در داخل یک هوک useCallback قرار می‌دهیم.

سینتکس هوک useCallback

سینتکس هوک useCallback به شکل زیر می‌باشد:

const cachedFn = useCallback(fn, [...dependencyArray])

هوک useCallback آرگومان‌های زیر را می‌گیرد:

در اولین رندر، React تابع را ایجاد کرده و آن را cache می‌کند. باید به این نکته توجه داشته باشیم که React آن تابع را فراخوانی نمی‌کند.

در رندرهای بعدی، این تابع که در حافظه cache ذخیره شده بود به ما return می‌شود. زیرا، هوک useCallback به جای مقدار بازگشتی تابع، خود تابع را return کرده و ذخیره می‌کند.

بررسی هوک useCallback در یک مثال

اکنون از هوک useCallback در مثالی که داریم استفاده می‌کنیم:

import { useCallback } from "react";

const App = () => {
  
  // ...

  const handleClick = useCallback(() => {
    setValue("Kunal");
  }, [value, setValue]);
  
  // ...
};

در این مثال، ما تابع را در داخل یک useCallback قرار داده‌ایم و دو dependency که با این تابع درگیر هستند را ارسال کرده‌ایم. اکنون، هنگامی که روی Increment Count کلیک می‌کنیم، رندر بسیار سریع‌تر است. دلیل این اتفاق این است که رفرنس handleClick بین رندرها ذخیره می‌شود و بنابراین SlowComponent دوباره رندر نمی‌گردد.

اما وقتی روی دکمه داخل SlowComponent کلیک کنیم دوباره رندر می‌شود. این به این دلیل است که وقتی state value تغییر می‌کند، متد handleClick دوباره ایجاد می‌شود و بنابراین props کامپوننت کُند تغییر کرده است.

تا زمانی که state value را به‌روزرسانی نکنیم، می‌توانیم stateهای بیشتری را به کامپوننت App اضافه نماییم و بدون اینکه بر روی performance برنامه تاثیر منفی بگذاریم، آن‌ها را به‌روزرسانی کنیم.

چه زمانی باید از هوک useCallback استفاده کنیم؟

تفاوت بین هوک useMemo و هوک useCallback چیست؟

در این بخش قصد داریم تا به بررسی تفاوت بین دو هوک useMemo و useCallback بپردازیم.

همچنین چند نکته دیگر که باید به آن‌ها توجه داشته باشیم این است که نباید از هوک‌های useMemo و useCallback در همه جا استفاده کنیم؛ فقط در صورتی می‌توانیم از این هوک‌ها در برنامه‌های خود بهره‌مند شویم که می‌خواهیم محاسبات پرهزینه را به خاطر بسپاریم یا از رندرهای غیرضروری جلوگیری کنیم.

برای عملکردهای معمولی، این هوک‌ها تفاوت چندانی ندارند. استفاده بیش از حد از آن‌ها کد ما را ناخوانا می‌کند. در عوض، می‌توانیم از راه‌های دیگری برای بهبود عملکرد اپلیکیشن خود بهره‌مند شویم.

جمع‌بندی

useMemo و useCallback هوک‌های مفیدی در React هستند که درک تفاوت بین این دو هوک و استفاده از آن‌ها می‌تواند به ما در بهینه‌سازی عملکرد برنامه‌هایی که داریم کمک کند.

در این مقاله نحوه عملکرد هر دو هوک را بررسی کرده‌ایم. هوک useMemo نتیجه یک محاسبه پرهزینه را در حافظه cache ذخیره می‌کند، در حالی که هوک useCallback رفرنس تابع را در حافظه cache ذخیره می‌نماید. ما همچنین سناریوهایی را بررسی کردیم که چه زمانی باید از هر هوک استفاده کنیم. هر دوی این هوک‌ها می‌توانند سرعت برنامه ما را افزایش داده و تجربه کاربری را بهبود بخشند.