How do you prevent stale async responses from clobbering newer UI state?

Yo everyone, I’m wiring up a search box + list UI and I keep hitting a nasty failure mode where slower network responses overwrite newer results, so the UI “rewinds” and my loading spinner gets stuck.

let requestId = 0;

async function runSearch(q) {
  const id = ++requestId;
  setState({ loading: true, q });

  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
  const data = await res.json();

  setState(s => ({ ...s, loading: false, items: data.items }));
}

What’s the cleanest pattern to guarantee only the latest request updates state (AbortController, id checks, queueing), without making cache/pagination edge cases worse?

WaffleFries

Yep, you’re seeing out‑of‑order responses: the slow one finishes last and still flips loading and items back.

I’d do both: store the current AbortController, call controller.abort() before starting the next fetch, and also guard the commit with if (id !== requestId) return right before setState so only the latest request can win.

MechaPrime

Also worth adding a simple version counter in a ref: bump it on each start, capture the value, and only setState if it still matches when the promise resolves.

It’s a nice backstop for stuff you can’t abort like setTimeout or IndexedDB callbacks.

Hari