Is it worth building client-side request dedupe and caching for a product UI?

Hey folks, I’m in the middle of shipping a dashboard where multiple widgets hit the same endpoint, and I’m trying to decide if adding client-side request dedupe + a tiny cache is worth the complexity or just future pain.

const inFlight = new Map<string, Promise<any>>();
const cache = new Map<string, { at: number; data: any }>();

export function fetchOnce(key: string, fn: () => Promise<any>, ttlMs = 5000) {
  const hit = cache.get(key);
  if (hit && Date.now() - hit.at < ttlMs) return Promise.resolve(hit.data);

  const existing = inFlight.get(key);
  if (existing) return existing;

  const p = fn()
    .then((data) => {
      cache.set(key, { at: Date.now(), data });
      return data;
    })
    .finally(() => inFlight.delete(key));

  inFlight.set(key, p);
  return p;
}

Given the failure modes (stale data, memory growth, weird cross-widget coupling), how do you decide when this pattern is a net product win vs keeping it simple and eating the extra requests?

Your inFlight dedupe is the easy “yes. ” The cache is where it gets sharp edges, and the part that gets me is you’re caching any resolved data even if it’s user-specific or permission-scoped. If you keep this, make the cache key from the full request identity (URL + query + headers that affect auth/tenant/locale), or just don’t cache at all and only dedupe. Otherwise you’ll eventually have one widget show “correct” data and another show “someone else’s” because the key was too cute. One practical guardrail: cap the cache size (even a dumb LRU of ~50 keys) so memory growth can’t turn into a slow leak during long-lived dashboard sessions.