Caching: پرهیز از درخواست‌های تکراری

در درس‌های قبل دیدیم چگونه یک درخواست را به‌درستی مدیریت، از Race Condition محافظت و در صورت نیاز لغو کنیم. اما یک سؤال دیگر باقی می‌ماند: اگر داده‌ای که می‌خواهیم، قبلاً یک‌بار گرفته شده، چرا باید دوباره همان درخواست را بفرستیم؟ Caching پاسخ همین سؤال است.

مشکل: درخواست تکراری برای داده‌ای که قبلاً داریم

فرض کنید کاربر بین چند Tab جابه‌جا می‌شود و هر Tab داده خودش را از سرور می‌گیرد. اگر کاربر به Tab اول برگردد، با پیاده‌سازی ساده useEffect که تا اینجا دیدیم، یک درخواست کاملاً جدید فرستاده می‌شود؛ حتی اگر همان داده، چند ثانیه پیش، دقیقاً از همان‌جا گرفته شده بود.

یک پیاده‌سازی ساده دستی

می‌توان یک حافظه موقت ساده، خارج از کامپوننت، برای نگه‌داشتن نتایج قبلی ساخت:

const cache = new Map();

function useCachedData(key, fetcher) {
  const [data, setData] = useState(() => cache.get(key) ?? null);

  useEffect(() => {
    if (cache.has(key)) {
      setData(cache.get(key));
      return;
    }

    fetcher().then((result) => {
      cache.set(key, result);
      setData(result);
    });
  }, [key]);

  return data;
}

در این نسخه ساده، با وجود کلید در cache، درخواست جدیدی فرستاده نمی‌شود و مقدار ذخیره‌شده بلافاصله استفاده می‌شود؛ فقط کلیدهای تازه، یک درخواست واقعی می‌فرستند.

چرا پیاده‌سازی دستی Cache به‌سرعت پیچیده می‌شود

نمونه بالا ساده‌سازی‌شده است و چند سؤال مهم را بی‌پاسخ می‌گذارد: داده ذخیره‌شده تا کی معتبر است؟ اگر داده روی سرور عوض شود، چه زمانی باید دوباره گرفته شود؟ اگر دو کامپوننت هم‌زمان همان کلید را بخواهند، آیا دو درخواست جدا می‌فرستند؟ پاسخ درست به این سؤال‌ها، نیازمند منطقی به‌مراتب کامل‌تر از یک Map ساده است.

مفهوم Stale Data: داده کهنه در برابر داده تازه

یک الگوی رایج، نمایش فوری داده قدیمی از Cache (حتی اگر ممکن است کمی کهنه باشد)، همراه با ارسال هم‌زمان یک درخواست تازه در پس‌زمینه برای به‌روزرسانی آن است. این رویکرد، که اغلب Stale-While-Revalidate نامیده می‌شود، باعث می‌شود کاربر هیچ‌وقت منتظر پیام Loading خالی نماند، در حالی که داده هم نهایتاً به‌روز می‌شود.

چرا این موضوع، نقطه عطف این فصل است

تا اینجا، با هر درس، یک لایه جدید از پیچیدگی به دریافت داده دستی اضافه کردیم: Loading و Error، سپس Race Condition، سپس Abort، و حالا Caching. این انباشته‌شدن منطق، دقیقاً همان دلیلی است که در عمل، اکثر برنامه‌های React واقعی، این کار را با یک کتابخانه تخصصی انجام می‌دهند، نه با useEffect دستی. در درس‌های بعدی همین فصل، به Server State در برابر Client State و سپس الگوهای رایج (از جمله همین کتابخانه‌ها) می‌پردازیم.

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

در این مثال، بار اول هر کلید با تأخیر بارگذاری می‌شود؛ اما بازگشت به کلیدی که قبلاً گرفته شده، بلافاصله و بدون تأخیر نمایش داده می‌شود.

یک حافظه موقت ساده برای پرهیز از درخواست تکراری
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Caching یعنی نگه‌داشتن نتیجه یک درخواست قبلی برای پرهیز از ارسال دوباره همان درخواست؛ پیاده‌سازی ساده آن با یک Map امکان‌پذیر است، اما سؤالاتی مثل اعتبار داده و هم‌زمانی چند درخواست، معمولاً نیاز به ابزاری کامل‌تر دارند. در درس بعدی به تفاوت اساسی Server State و Client State می‌پردازیم.