useCallback: پایدار نگه‌داشتن مرجع یک تابع

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

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

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

  function handleClick() {
    console.log("کلیک شد");
  }

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <MemoizedButton onClick={handleClick} />
    </div>
  );
}

با اینکه MemoizedButton با React.memo پیچیده شده، با هر کلیک روی دکمه شمارنده، handleClick دوباره ساخته می‌شود و مرجعی متفاوت از Render قبلی دارد. طبق چیزی که در دو درس قبل دیدیم، این یعنی مقایسه سطحی memo، این Prop را «تغییر‌کرده» تشخیص می‌دهد و MemoizedButton بی‌دلیل دوباره Render می‌شود.

راه‌حل: useCallback

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

  const handleClick = useCallback(() => {
    console.log("کلیک شد");
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <MemoizedButton onClick={handleClick} />
    </div>
  );
}

useCallback، تابعی که به آن می‌دهیم را می‌گیرد و تا وقتی Dependency Array تغییر نکرده، همان تابع (با همان مرجع) از Render قبلی را برمی‌گرداند؛ نه یک نسخه تازه. حالا MemoizedButton با هر کلیک روی شمارنده، دوباره Render نمی‌شود.

useCallback در واقع یک مورد خاص از useMemo است

این دو خط، از نظر نتیجه، دقیقاً معادل هم هستند:

const fn = useCallback(() => doSomething(a, b), [a, b]);
const fn = useMemo(() => () => doSomething(a, b), [a, b]);

useCallback فقط یک نحو کوتاه‌تر برای همین کار است، مخصوص زمانی که خود مقدار موردنظر، یک تابع است. به همین دلیل، تمام نکاتی که درباره useMemo در درس قبل گفتیم (هزینه داشتن، نیاز به Dependency Array درست) عیناً درباره useCallback هم صادق است.

چه زمانی useCallback واقعاً لازم است

useCallback فقط زمانی سودمند است که مرجع پایدار تابع، جایی واقعاً مورد استفاده قرار گیرد؛ یعنی یا به‌عنوان Prop به یک کامپوننت memo‌شده داده شود، یا در Dependency Array یک useEffect یا Hook دیگر قرار گیرد. اگر تابعی فقط مستقیماً در onClick یک عنصر معمولی HTML استفاده می‌شود و به‌جای دیگری پاس داده نمی‌شود، پیچیدن آن با useCallback معمولاً هیچ سودی ندارد.

یک اشتباه رایج: فراموش کردن Dependency تابع داخلی

اگر تابع داخل useCallback به مقداری از بیرون (مثلاً یک State یا Prop) وابسته باشد، آن مقدار باید در Dependency Array باشد؛ وگرنه همان مشکل Stale Closure که در فصل ششم دیدیم، اینجا هم رخ می‌دهد: تابع برای همیشه به مقدار قدیمی آن متغیر اشاره می‌کند.

const handleSave = useCallback(() => {
  saveData(text); // text باید در Dependency Array باشد
}, [text]);

مثال قابل اجرا

در این مثال، MemoizedButton فقط یک‌بار Render می‌شود، حتی با کلیک‌های مکرر روی دکمه شمارنده؛ چون handleClick مرجع ثابتی دارد.

جلوگیری از Re-render فرزند با useCallback
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useCallback مرجع یک تابع را بین Renderها پایدار نگه می‌دارد، تا وقتی Dependencyهایش تغییر نکرده باشند؛ از نظر مکانیزم، معادل useMemo برای یک تابع است. این ابزار وقتی ارزش دارد که آن تابع به یک کامپوننت memo‌شده داده شود یا در Dependency Array جای دیگری استفاده شود. در درس بعدی، Memoization و Trade-offهای آن را جمع‌بندی می‌کنیم.