Pada aplikasi React Native skala menengah hingga besar, skenario umum seperti perpindahan ke tab dashboard sering memicu render bersamaan pada beberapa komponen independen (misalnya avatar header, widget saldo, dan banner notifikasi). Jika masing-masing komponen memanggil endpoint data profil pengguna (/api/v1/profile) saat mount, aplikasi akan mengirimkan sejumlah HTTP request identik ke server dalam rentang beberapa milidetik. Fenomena ini memicu thundering herd problem atau cache stampede di sisi client, yang mengakibatkan lonjakan penggunaan bandwidth seluler, latensi render, dan potensi respons HTTP 429 Too Many Requests dari API gateway.

Akar Masalah: Thundering Herd di React Native

Cache stampede di lingkungan mobile umumnya berakar dari tiga kondisi:

  1. Siklus Hidup Komponen Terisolasi: Komponen UI dirancang modular dengan pemanggilan data mandiri via useEffect atau custom hook tanpa sinkronisasi global.
  2. Cold Start & Cache Miss Serentak: Saat cache lokal kedaluwarsa atau belum terbentuk, seluruh komponen mengevaluasi bahwa data tidak ada dan secara bersamaan memicu network request.
  3. Asinkronitas Tanpa Kunci (Locking Mechanism): Request pertama belum selesai dieksekusi (masih berstatus pending), sehingga request kedua dan seterusnya tidak mendeteksi bahwa proses pengambilan data sedang berlangsung.

Arsitektur Solusi: In-Flight Promise Deduplication

Solusi paling efisien untuk masalah ini adalah in-flight request deduplication. Mekanisme ini menahan referensi Promise yang sedang berjalan di dalam memori transient (Map). Setiap kali komponen meminta data dengan kunci identik:

  • Jika Promise untuk kunci tersebut sedang berjalan, kembalikan referensi Promise yang sama.
  • Jika tidak ada Promise aktif, buat request baru, daftarkan ke Map, dan hapus dari Map segera setelah selesai (baik sukses maupun gagal).

Penyimpanan persisten lokal ditangani oleh MMKV (via react-native-mmkv) karena operasi sinkron dan latensi I/O yang sangat rendah dibanding AsyncStorage.

Implementasi: Dedupe Client Terintegrasi MMKV

Berikut implementasi client fetcher dengan proteksi cache stampede dan SWR (Stale-While-Revalidate):

import { MMKV } from 'react-native-mmkv';

const storage = new MMKV();

interface CacheEnvelope<T> {
  data: T;
  cachedAt: number;
  ttl: number;
}

interface FetchOptions {
  ttl?: number;      // Waktu validitas data segar (ms)
  staleTtl?: number; // Batas toleransi data basi saat revalidasi (ms)
}

// In-memory registry untuk request yang sedang berjalan
const inFlightRequests = new Map<string, Promise<any>>();

export async function dedupeFetch<T>(
  key: string,
  fetcher: () => Promise<T>,
  options: FetchOptions = {}
): Promise<T> {
  const { ttl = 60000, staleTtl = 300000 } = options;
  const now = Date.now();

  // 1. Periksa Cache Persisten MMKV
  const cachedRaw = storage.getString(key);
  if (cachedRaw) {
    try {
      const envelope: CacheEnvelope<T> = JSON.parse(cachedRaw);
      const age = now - envelope.cachedAt;

      // Data masih sepenuhnya segar
      if (age <= ttl) {
        return envelope.data;
      }

      // Data basi tetapi masih dalam jendela stale-while-revalidate
      if (age <= ttl + staleTtl) {
        // Trigger background revalidation jika belum ada request berjalan
        revalidateInBackground(key, fetcher, ttl);
        return envelope.data;
      }
    } catch {
      storage.delete(key);
    }
  }

  // 2. Evaluasi In-Flight Promise Pool
  const existingPromise = inFlightRequests.get(key);
  if (existingPromise) {
    return existingPromise as Promise<T>;
  }

  // 3. Eksekusi Request Baru dengan Lifecycle Safe Cleanup
  const requestPromise = (async () => {
    try {
      const result = await fetcher();
      const envelope: CacheEnvelope<T> = {
        data: result,
        cachedAt: Date.now(),
        ttl,
      };
      storage.set(key, JSON.stringify(envelope));
      return result;
    } finally {
      // Wajib: Bersihkan pool agar tidak terjadi memory leak atau deadlock
      inFlightRequests.delete(key);
    }
  })();

  inFlightRequests.set(key, requestPromise);
  return requestPromise;
}

function revalidateInBackground<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number
): void {
  if (inFlightRequests.has(key)) return;

  const bgPromise = fetcher()
    .then((result) => {
      const envelope: CacheEnvelope<T> = {
        data: result,
        cachedAt: Date.now(),
        ttl,
      };
      storage.set(key, JSON.stringify(envelope));
    })
    .catch((err) => {
      // Log error background tanpa menginterupsi UI
      console.warn(`Revalidation failed for ${key}:`, err);
    })
    .finally(() => {
      inFlightRequests.delete(key);
    });

  inFlightRequests.set(key, bgPromise);
}

Penanganan Lifecycle dan Pencegahan Deadlock

Kesalahan fatal dalam implementasi promise pool adalah membiarkan promise berstatus rejected tersimpan di dalam Map. Jika blok finally diabaikan:

  • Komponen yang memanggil fungsi berikutnya akan terus menerima rejected promise yang sama tanpa pernah mencoba network call baru.
  • Aplikasi mengalami kondisi pseudo-deadlock, di mana data tidak pernah diperbarui hingga aplikasi di-restart secara penuh (proses JS bundle dimulai ulang).

Penggunaan blok finally menjamin pembersihan kunci dari inFlightRequests secara deterministik, baik saat HTTP call sukses, timeout, maupun mengalami network failure (e.g., DNS resolution drop).

Panduan Konfigurasi Stale-While-Revalidate TTL

Arsitektur mobile berhadapan langsung dengan latensi koneksi radio seluler yang fluktuatif. Pola konfigurasi TTL yang direkomendasikan dibagi berdasarkan karakteristik data:

  • Data Statis/Katalog (Category, Config, Assets): ttl: 3600000 (1 jam), staleTtl: 86400000 (24 jam). Memastikan UI langsung muncul saat cold start.
  • Data Pengguna Semi-Dinamis (Profile, Entitlements): ttl: 60000 (1 menit), staleTtl: 300000 (5 menit). Mengeliminasi request saat navigasi antar tab layar.
  • Data Finansial/Transaksi Kritis (Balance, Cart): ttl: 0, staleTtl: 0. Melewati storage MMKV secara langsung, namun tetap memanfaatkan in-flight dedupe untuk meredam pemanggilan serentak dari multiple mounts.

Verifikasi Hasil Profiling Network

Efektivitas mekanisme ini dapat diverifikasi menggunakan Reactotron, Flipper Network Plugin, atau Charles Proxy:

Baseline (Tanpa Dedupe): 4 komponen me-mount halaman dashboard secara bersamaan. Inspector mencatat 4 koneksi HTTP terpisah ke endpoint /api/v1/profile dengan status 200/429, durasi rata-rata 320ms per request, dan bandwidth terbuang 4x lipat.

Setelah Dedupe Aktif: Dari 4 komponen yang me-mount bersamaan, hanya tercatat 1 HTTP request fisik pada inspector. Tiga pemanggil lainnya menerima resolusi dari memory promise yang sama secara lokal dengan waktu eksekusi < 1ms setelah data tiba.

Pendekatan ini memangkas round-trip time jaringan secara drastis pada cold start dan sepenuhnya menghilangkan risiko rate-limiting 429 akibat multi-mount komponen di React Native.