useMemo: پایدار نگه‌داشتن یک مقدار محاسبه‌شده بین Renderها

در درس قبل دیدیم که هر Object یا Array تازه‌ساخته‌شده در بدنه کامپوننت، در هر Render مرجع جدیدی دارد. useMemo ابزاری است که به ما اجازه می‌دهد یک مقدار محاسبه‌شده را، تا وقتی ورودی‌هایش تغییر نکرده، بین Renderهای مختلف با همان مرجع قبلی نگه داریم.

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

function ProductList({ items, query }) {
  const filtered = items.filter((item) => item.name.includes(query));
  // اگر items هزاران آیتم داشته باشد، این فیلتر با هر Render دوباره اجرا می‌شود
  return <ul>{filtered.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}

اگر ProductList به هر دلیلی (مثلاً تغییر یک State بی‌ربط در والد) دوباره Render شود، این عملیات filter هم دوباره روی کل آرایه اجرا می‌شود؛ حتی اگر نه items و نه query تغییر کرده باشند.

راه‌حل: useMemo

function ProductList({ items, query }) {
  const filtered = useMemo(() => {
    return items.filter((item) => item.name.includes(query));
  }, [items, query]);

  return <ul>{filtered.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}

useMemo یک تابع و یک Dependency Array می‌گیرد؛ دقیقاً مثل useEffect. اما به‌جای اجرای Effect پس از Commit، تابع را همان لحظه، در حین Render، اجرا می‌کند و نتیجه آن را برمی‌گرداند. تا وقتی items و query نسبت به Render قبلی تغییر نکرده باشند، React محاسبه را دوباره اجرا نمی‌کند و همان نتیجه قبلی (با همان مرجع) را برمی‌گرداند.

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

همان‌طور که در دو درس قبل دیدیم، یک Object تازه‌ساخته‌شده، React.memo را بی‌اثر می‌کند. useMemo این مشکل را هم حل می‌کند:

function Parent({ count }) {
  const style = useMemo(() => ({ color: "red" }), []);
  return <MemoizedCard style={style} />;
}

حالا style فقط یک‌بار ساخته می‌شود (چون Dependency Array خالی است) و در هر Render بعدی Parent، همان مرجع قبلی بازگردانده می‌شود؛ پس MemoizedCard با memo، دیگر به‌خاطر این Prop دوباره Render نمی‌شود.

useMemo رایگان نیست

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

یک اشتباه رایج: فراموش کردن Dependency Array درست

دقیقاً مثل useEffect، هر مقداری که داخل تابع useMemo از بیرون خوانده می‌شود، باید در Dependency Array باشد؛ در غیر این صورت، همان مشکل Stale Closure که در فصل ششم دیدیم، اینجا هم رخ می‌دهد و مقدار محاسبه‌شده، با داده واقعی هماهنگ نمی‌ماند.

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

در این مثال، فیلتر کردن لیست فقط وقتی query یا items تغییر کند دوباره اجرا می‌شود، نه با هر کلیک روی دکمه شمارنده.

جلوگیری از محاسبه تکراری با useMemo
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

useMemo یک تابع را در حین Render اجرا می‌کند و نتیجه آن را، تا وقتی Dependencyها تغییر نکرده، با همان مرجع قبلی نگه می‌دارد. این ابزار برای دو هدف اصلی مفید است: پرهیز از تکرار یک محاسبه سنگین، و پایدار نگه‌داشتن مرجع یک Object یا Array برای Dependency Array یا React.memo. در درس بعدی، به useCallback می‌پردازیم که همین ایده را برای توابع به کار می‌برد.