Why does my optimistic update sometimes get overwritten by older fetch results?

Hey everyone, I’m wiring up a tiny product list UI and trying to make deletes feel instant, but now and then an older fetch result strolls in and puts the item back like nothing happened.

let state = { items: [] };
let reqId = 0;

async function refresh() {
  const id = ++reqId;
  const data = await fetch('/api/items').then(r => r.json());
  if (id === reqId) state.items = data;
}

async function removeItem(id) {
  const prev = state.items;
  state.items = state.items.filter(x => x.id !== id);
  try {
    await fetch(`/api/items/${id}`, { method: 'DELETE' });
    refresh();
  } catch {
    state.items = prev;
  }
}

What is the least ugly way to stop stale refreshes from resurrecting deleted items without turning this into a homegrown state machine?

Arthur

@Arthur reqId only protects fetch order. It does not remember that you already hid item 42 locally, so a perfectly valid later refresh() can still put it back.

The cleanest small fix is to track ids that are “being deleted” and filter fetched data against that set until the DELETE either succeeds or fails:


js
let state = { items: [] };
let reqId = 0;
const pendingDeletes = new Set();

async function refresh() {
  const id = ++reqId;
  const data = await fetch('/api/items').then(r => r.json());

  if (id === reqId) {
    state.items = data.filter(item => !pendingDeletes.has(item.id));
  }
}

async function removeItem(id) {
  const prev = state.items;
  pendingDeletes.add(id);
  state.items = state.items.filter(x => x.id !== id);

  try {
    await fetch(`/api/items/${id}`, { method: 'DELETE' });
    pendingDeletes.delete(id);
    refresh();
  } catch {
    pendingDeletes.delete(id);
    state.items = prev;
  }
}

That way if an older /api/items response comes back with the deleted row still in it, refresh() strips it out instead of resurrecting it.

BayMax