Preventing UI race conditions in ad submission forms

Summary

A Submit Ads form was failing to display newly created advertisements on the homepage. The issue stemmed from a race condition between the Ajax request that creates an ad and the DOM update that appends it to the latest ads list.

Key Takeaway: Ensure that UI updates occur only after the server confirms success and that data is re‑fetched or properly merged.

Root Cause

  • The form’s submit handler used fetch('/ads') without awaiting the server’s response before calling document.querySelector('.latest-ads').appendChild(newAdElement);.
  • The server responded asynchronously; meanwhile the client attempted to render a draft ad object that was not yet persisted.
  • Because the draft had an id of undefined, it was never included in the homepage API response, leaving the list stale.

Why This Happens in Real Systems

  • Optimistic UI updates are common for perceived performance but can introduce inconsistencies.
  • Asynchronous data mutations that don’t return the updated state to the client.
  • Caching layers that serve stale data while a new record is still being written.

Real-World Impact

  • Users could submit an ad but no visual confirmation appeared, leading to confusion and repeat submissions.
  • Backend databases held duplicate drafts or orphaned records when the page refreshed.
  • The team’s dashboards reported fewer ads than actual, affecting analytics and revenue projections.

Example or Code

// Vulnerable submit handler
document.getElementById('submit-ad').addEventListener('click', () => {
  const adData = { title: 'New Ad', body: 'Great product' };
  fetch('/api/ads', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(adData),
  });

  // Immediately update UI (optimistic)
  const adEl = document.createElement('div');
  adEl.textContent = adData.title;
  document.querySelector('.latest-ads').appendChild(adEl);
});

How Senior Engineers Fix It

  • Await the server response and use the returned ad data for rendering.

  • Refactor to optimistic updates only when the API guarantees immediate persistence or returns the new ID.

  • Integrate a state‑management library (e.g., Redux, Zustand) to sync server state with the UI.

  • Add pessimistic UI updates for critical flows: wait for confirmation before rendering.

    document.getElementById('submit-ad').addEventListener('click', async () => {
    const adData = { title: 'New Ad', body: 'Great product' };
    const response = await fetch('/api/ads', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(adData),
    });
    const result = await response.json();
    
    if (result.success) {
      const adEl = document.createElement('div');
      adEl.textContent = result.ad.title;
      document.querySelector('.latest-ads').appendChild(adEl);
    } else {
      alert('Error submitting ad');
    }
    });

Why Juniors Miss It

  • Assuming immediate local changes reflect persisted state without validation.
  • Underestimating asynchronous behavior and missing the need to await.
  • Overreliance on optimistic updates without fallback logic.

Bottom line: Always align your UI with the actual data source, or at least confirm changes before displaying them.

Leave a Comment