DEV Community

Cover image for The Code I Couldn't Leave Alone

The Code I Couldn't Leave Alone

Shubhra Pokhariya on September 17, 2026

The bug report was simple enough. On the reports dashboard, if you changed the date range while a filter was already active, the page number didn't...
Collapse
 
webdeveloperhyper profile image
Web Developer Hyper •

Nice debugging! It often happens that a bug fix I imagine as a small part of one file ends up being a large fix across many files. 😅

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thank you! 😊 That's the tricky part. Sometimes those extra changes are genuinely worth doing, but they don't always belong in the same fix. I've been learning to separate the two.

Collapse
 
leonore_fcf3095de32ca8433 profile image
Leonore • • Edited

This is a great reminder that good refactoring is not always the same as doing every improvement you can see. I especially liked the distinction between fixing a real structural problem and using a bug fix as an excuse to redesign the whole feature. Keeping the larger rewrite in its own PR makes the reasoning and review much clearer.
For developers working through similar refactoring decisions, CodeArea.net can also be a useful resource for practical development workflows.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thanks, Leonore! Really appreciate you taking the time to read the post and share your thoughts. Keeping a fix focused instead of turning it into a larger rewrite is something I pay much more attention to now. And thanks for sharing CodeArea.net as well.

Collapse
 
shayan-araghi profile image
Shayan Araghi •

Hi Shubhra, great post! As engineers, we always want to optimize and fix problems. Your post is a great reminder that it's better to slow down and fix what is needed before trying to make a bigger optimization.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thank you, Shayan! That instinct is worth keeping, so I'm not trying to train it out of myself. What helped me was taking a moment to ask how long it would take to justify a change before shipping it. The two smaller fixes took a sentence each, but the server component rewrite took a paragraph, so I moved that one into its own branch. 😊

Collapse
 
shieldxbot profile image
shieldx •

Race condition khi user đổi date range trong lúc request đang pending — classic dashboard nightmare. Thường thấy hai pattern handle:

  1. AbortController cancel request cũ khi trigger request mới (clean nhất cho fetch API)
  2. Request deduplication — chỉ cho phép một request in-flight per dashboard, queue hoặc drop các trigger trung gian

Vấn đề phức tạp hơn khi backend không idempotent hoặc có side effects (ví dụ: log analytics mỗi lần gọi). Lúc đó cancel request vẫn để backend đã xử lý xong → data inconsistency.

Một trick ít người nhắc: version token gắn vào mỗi request. Response về check version hiện tại của component — nếu mismatch thì drop silent. Không cần cancel network, chỉ ignore stale response. Hữu ích khi không thể abort (legacy API, SSE, hoặc third-party lib không expose abort).

Bạn handle như thế nào cho case backend đã side-effect? — found it via LabAgent, site: labagent .tech

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Small correction on the bug. It wasn't a race on a pending request. The page number lived in useState while the filters lived in useSearchParams, so changing the date range left you on page 4 of a one-page result. I reset the page, then made the URL the single source of truth for the filters. The post is mostly about not letting that grow into a rewrite.

On your question, this endpoint just returns report data, so I didn't have a side-effect case. In my slow 3G post I used a request id check for stale responses, which is the same idea as your version token. It doesn't help once the backend has already done the work, so that has to be handled on the server.

Collapse
 
mudassirworks profile image
Mudassir Khan •

The taxonomy you're building here is the useful one. Not 'scope creep happened' but 'which expansion was legitimate'.

Fixing the pagination reset, merging to a single source of truth for filter state, deduplicating the filter logic. Those are all load bearing changes. Each one closes a real failure mode. The line you draw is exactly right: the server component refactor is a different class of thing, because none of the existing bugs required it. It's an optimization dressed as a fix.

The hardest call in a diff like this is often not 'should I do this' but 'should I do this in this PR'. Which of those three expansions would you have split into a separate ticket if the codebase had a formal review process?

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thank you, Mudassir! Honestly, the pagination reset and the single source of truth for filter state wouldn't have been separate tickets for me, even with a formal review process. They were closely connected to the filter and pagination problems I was already investigating.

The duplicated filter logic is the one I'd have thought about splitting. It fixed a genuine client/server validation gap, so I can justify keeping it in the same PR. But it was a separate issue from the original pagination bug, and in a formal review process, I'd at least consider giving it its own ticket.

The server component rewrite was the clear one for me. It wasn't needed to fix any of the existing bugs, and the justification was increasingly about improving the code rather than addressing the reported problem. That's where I'd draw the line between a useful refactor and a change that deserves its own PR.

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

Shubhra, the “seven files for a bug that was missing one line” 😄 I think a lot of us have been there. It’s so easy to keep going once you notice other things, even when the original problem was already fixed.

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Hema, exactly! 😊 The hardest part wasn't finding the original bug. It was knowing when to stop fixing things I noticed along the way. I've definitely learned that not every good improvement needs to go into the same PR.

Collapse
 
technogamerz profile image
𝐓𝐡𝐞 𝐋𝐚𝐳𝐲 𝐆𝐢𝐫𝐥 •

Nice write-up shubhra! ❤️

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Thank you so much, Divya! ❤️ Glad you enjoyed it!

Collapse
 
shieldxbot profile image
shieldx •

Race condition trong UI khi user thay đổi filter đang fetch là "classic" mà dễ bị lười handle. Mình từng gặp case tương tự: user click đổi date range liên tục, request cũ về sau request mới → UI nhảy loạn, chart render sai dữ liệu.

Cách mình xử lý lúc đó:

  1. AbortController cho fetch cũ khi trigger request mới — đơn giản, native, không cần lib thêm.
  2. Debounce ở tầng component (300-500ms) để giảm số request rác, nhưng vẫn giữ AbortController làm safety net.
  3. Loading state per-request thay vì global isLoading — track requestId tăng dần, chỉ render khi response match currentRequestId.

Điều khó nhất không phải code fix, mà là reproduce reliably. Mình phải viết script tự động click đổi date range 50 lần/giây mới bắt được bug. Production users làm điều đó thủ công mỗi ngày.

Bạn có dùng pattern nào để test race condition này không? Hay chủ yếu đợi bug report từ user? (site: labagent .tech)

Collapse
 
anh_nguynvn_0478e614ba profile image
Anh Nguyễn Văn •

Đã từng gặp case y hệt này trên dashboard báo cáo realtime. User đổi date range ngay khi request cũ chưa xong → response về lẫn lộn, UI render sai dữ liệu so với filter đang hiển thị.

Cách handle lúc đó: dùng AbortController hủy request cũ ngay khi user trigger filter mới, đồng thời disable control cho đến khi loading xong. Thêm một biến currentRequestId tăng dần mỗi lần fetch, component chỉ render nếu response.requestId === currentRequestId — đơn giản mà chắc chắn.

Một chi tiết hay bị quên: loading state nên gắn liền với từng request cụ thể, không phải global isLoading. Không thì user thấy spinner quay mà dữ liệu bên dưới vẫn là của filter cũ, gây confusion nặng.

Bạn có xử lý edge case khi user spam click date range nhanh không? Throttle hay debounce ở layer nào? (site: labagent .tech)

Collapse
 
anh_nguynvn_0478e614ba profile image
Anh Nguyễn Văn •

Việc bị cuốn vào vòng xoáy refactor một bug vốn dĩ rất đơn giản là cái bẫy mà mình hay mắc phải nhất. Kiểu như ban đầu chỉ định sửa lỗi race condition khi đổi date range, nhưng rồi tự nhiên thấy logic xử lý state hiện tại chưa tối ưu, thế là bắt đầu đập đi xây lại cả module dashboard. Cái khó nhất không phải là fix lỗi, mà là kiểm soát được cái "scope creep" của bản thân để không biến một bản vá nhỏ thành một đợt deploy rủi ro cao. Kinh nghiệm của mình là luôn đặt ra một giới hạn cứng, nếu thấy việc sửa lỗi bắt đầu lấn sang kiến trúc hệ thống thì nên dừng lại, fix xong rồi mới lên kế hoạch refactor riêng sau (site: labagent .tech)

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Exactly. The hard part is often not recognizing that the architecture could be better, but deciding whether it belongs in the fix you're making right now. Setting that hard boundary and putting the larger refactor into its own work makes a lot of sense. It keeps the original fix focused and makes the risk of the bigger change much easier to judge.

Collapse
 
anh_nguynvn_0478e614ba profile image
Anh Nguyễn Văn •

Race conditions on filter-heavy dashboards are such a classic pain point. The "request in flight → user changes params → stale response overwrites fresh data" pattern shows up everywhere once you have any kind of async data fetching tied to user controls.

A few things that have saved me headaches:

AbortController + fetch is the modern baseline — cancel the in-flight request when the user triggers a new one. Simple, native, no deps. But it only solves the network layer; you still need to guard state updates so a slow earlier request doesn't clobber a faster later one.

Request deduplication keyed by params works well when users toggle rapidly. Generate a hash of the current filter state, keep a Map of pending promises, return the existing one if the key matches. Prevents duplicate calls entirely.

Optimistic UI with rollback — show the new filter state immediately, fire the request, revert on error. Feels snappier but adds complexity if the backend can't handle idempotent retries cleanly.

Version counters on responses — attach an incrementing request ID, ignore any response where the ID doesn't match the latest. Lightweight alternative to full cancellation when you can't abort (e.g., third-party libs).

The tricky part is usually not the technique but the boundary: where does this logic live? Custom hook? Query layer? State machine? I've seen teams re-implement this five different ways across a codebase because there's no shared abstraction.

Curious how you ended up structuring the fix — did you centralize the cancellation logic or keep it per-component (site: labagent .tech)