LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #22: Click Race Condition – When User's click speed beats your App
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

Blog #22: Click Race Condition – When User's click speed beats your App

Analyzing the decision to handle high-frequency interactions in a B2B Dashboard with millions of records and the 'Mismatched Data' incident.

TP
Truong PhamSoftware Engineer
PublishedMay 15, 2024

I am managing a team of 4 developers building a Logistic Management Dashboard. The average data scale is about 10,000 waybills per page. The deadline is looming and we've encountered a "bittersweet" problem: Users (those coordinating warehouses) have extremely fast mouse click speeds. They can click and select 5-7 different filters in just 2 seconds.

The Problem: When data "can't catch up" with the UI

The problem appears when requests sent out have different response times. Request #1 (filter by date) takes 1000ms, while Request #2 (filter by warehouse) takes 200ms. The result: Request #2 finishes first, and the UI displays warehouse data. Then Request #1 finishes and overwrites the UI, displaying date data. At this point, the User sees the "Warehouse" filter selected, but the figures are for "Date". A true data disaster!

Options Considered

Our team discussed and came up with two ways to solve it:

Option 1: Use UI Block (The Safe Road)

  • Solution: When a user clicks, show a loading Overlay or Disable all buttons until there's a result.
  • Pros: Extremely easy to code; never worry about race conditions (as only 1 request is out at a time).
  • Cons: Disaster for UX. Users feel a "hitch" with every operation. For a dashboard needing speed, this is unacceptable.

Option 2: Request Cancellation with AbortController (The Techy Road)

  • Solution: As soon as there's a new request, we "cancel" (abort) the old request in flight. At the same time, use Optimistic UI so users see the filter has been received.
  • Pros: Smooth UX; users can click as much as they want; the app always displays the result of the last click.
  • Cons: More complex logic; need to handle AbortError everywhere to avoid false error reports to users.

Final Decision and Analysis

I decided to choose Option 2.

// Example of automatic request cancellation structure
const searchProducts = (params, signal) => {
  return fetch(`/api/products?${params}`, { signal });
};

// Inside Component/Hook
useEffect(() => {
  const controller = new AbortController();
  
  fetchData(params, controller.signal)
    .then(data => setResults(data))
    .catch(err => {
      if (err.name === 'AbortError') return; // Ignore errors from canceled requests
      showError(err);
    });

  return () => controller.abort(); // Cleanup when component unmounts or params change
}, [params]);

Impact on Performance: The Network Tab looks "cleaner" because old requests are canceled (status: Canceled), freeing up bandwidth for the latest request. The CPU no longer has to handle rendering data sets that have been discarded.

Impact on Maintainability: We had to write a custom useFetch hook to encapsulate all the AbortController logic. This transforms a complex logic into a single line of code for future developers to reuse.

Impact on Team: The Juniors were initially unfamiliar with the AbortController concept, but after explaining that it's like "canceling a just-placed order to place a new one," everyone caught on quickly and applied it consistently.

Self-Reflection: Was it Over-engineering?

I wonder: Why not just use debounce for simplicity? In reality, debounce only helps reduce the number of requests sent; it doesn't solve the "first request arriving after the second request" problem. AbortController is the most fundamental solution.

If I went back to that time, would I choose differently? No. Dashboard UX is the number 1 priority. Investing an extra 2 days to handle a sustainable request architecture is a completely worthwhile trade-off compared to letting users get frustrated by "dancing" data every day.


Notes on mastering the rhythm between user and server.

Series • Part 22 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #23: Multi-tab Syncing – When your application 'talks' between tabs
Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument
17Blog #17: Images vs JS – The battle for priority in the Critical Path18Blog #18: Laggy Animations – When the touch is no longer smooth19Blog #19: Chrome Performance Tab – Looking through a microscope at the system's pulse20Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?21Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument22Blog #22: Click Race Condition – When User's click speed beats your AppReading23Blog #23: Multi-tab Syncing – When your application 'talks' between tabs24Blog #24: Failed Optimistic Update – When efforts to 'smooth' UI turn into UX disasters25Blog #25: Context API Perf Hit – When the 'standard React' solution betrays you26Blog #26: Don't let 'Single Source of Truth' become blind dogma27Blog #27: useEffect is not the place for doing all synchronizing logic
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles