skip to content

Duplicate records appear although the submit control is disabled while a request is in flight. What got past the guard?

level: seniorimportance: should knowfreq 50%

answer

  1. a disabled control closes one path
  2. five ways in, not one
  3. the rendered flag can be late
  4. read the guard synchronously
  5. a request key per attempt, not per retry

basics

~20 s

A disabled control closes only its own path. Submission can start elsewhere, the flag may read stale, a remount resets it, a lost response gets retried. Real protection: a synchronous in-flight check plus an idempotent server write.

solid answer

~50 s

A disabled control guards one path: presses on that control. Duplicates come from the others. A submission can be started by a second control in the form, by a keyboard shortcut, by a programmatic submit, or by a second mounted copy of the form. The flag itself can be late: in a runtime that re-runs the component per state write, the handler reads the value captured in its own run, so two events in the same frame both see `false`. So the guard must be a value the handler reads and writes synchronously, not a rendered one, and setting it after an `await` leaves the window open anyway. A remount resets it while the request is still open. And a retry after a lost response is not a UI bug: the first write landed, so the last layer is a request key, generated once per attempt and honoured by the server.

go deeper

for a junior

Know that disabling the submit control while a request is open is good practice but not a real guarantee, and that two presses can still produce two records. Say that the server has a part to play.

for a middle

Explain the paths that bypass one control's disabled state, and why a rendered flag can still read stale inside the handler that just wrote it. Describe holding the in-flight request instead of a boolean.

for a senior

Walk the whole list: other triggers, same-frame events, a flag set after an await, a remount during flight, and a retry after a lost response. Pair the client guard with an idempotent write and say which layer covers which case.

for a principal

Treat duplicate writes as a measured property of the system, with a rate per endpoint and a request-key convention every write endpoint honours, rather than a habit each form author is expected to remember.

## Why the disabled control is not the guard Disabling the submit control while a request is open stops one thing: another press on that control. It is worth having — it is the cheapest feedback in the form — but it is not where duplicate protection lives. Enumerate the paths that bypass it: 1. **Another control starts the submission.** A second button with a different intent, a control elsewhere on the page, a shortcut key bound to save. Only the disabled one is disabled. 2. **The flag is not readable yet.** Two events land in the same frame; both handlers read the value as it was before either wrote it. 3. **The flag is set too late.** The handler awaits something — reading a file, asking for confirmation, computing a token — and only then marks itself pending. Everything before that point is an open window. 4. **The component remounts.** A navigation, a changed key, a parent re-creating the subtree: the flag is local state and it comes back as idle while the earlier request is still in flight. 5. **The response is lost, not the write.** The user retries, or the client retries automatically, and the server records a second write because the first one succeeded and only its answer went missing. Only the first four are client-side bugs. The fifth is the reason a purely client-side story can never be complete. ## The flag has to be readable synchronously Rendered state is designed to drive rendering, and how fast a write becomes visible to a reader depends on the reactivity model. In a runtime that **re-runs the component function** on each write, the handler closes over the values from the run that created it, so a value it just wrote still reads as the old one for the rest of that handler — and for any other handler created in the same run. In a **fine-grained** runtime, a write is visible to the next read immediately. In a **compile-time** runtime the assignment becomes a notification, and the read may or may not be reordered before the re-render. Because the answer differs, the portable guard does not depend on it: hold the in-flight marker in something the handler reads and writes directly — a mutable holder attached to the component instance, or the in-flight request itself, kept in a module-scoped map keyed by the record being written. The rendered flag remains, but it now reflects the guard rather than being it. A neat expression of the same idea: keep the in-flight promise. If one exists for this form, the handler returns it instead of starting a second request. The guard and the deduplication are then the same object. ## Where a framework-tracked submission helps When the framework itself tracks a submission started through the form as one unit of work, with its own pending status and result, two of the five paths close for free: the tracked status covers every path the framework routes through that submission, and the status settles even when the handler throws. It does not close the remount case, and it certainly does not close the lost-response case. So a tracked submission replaces your flag; it does not replace your idempotency story. ## The retry case: idempotency A write whose response is lost is indistinguishable, from the client, from a write that never happened. The only correct resolution is for the second attempt to be recognisable as the same attempt: - Generate a **request key once per user attempt** — not once per retry — and send it with every retry of that attempt. - The server records the key with the write and, on seeing it again, returns the original result instead of writing again. - A natural uniqueness constraint on the record can substitute where one exists, at the cost of a much worse error to present. The key must be regenerated when the user genuinely starts a new attempt, or a legitimate second submission will be swallowed as a duplicate. ## Layers, in order of strength | Layer | Closes | Leaves open | |---|---|---| | Disabled submit control | repeat presses on that control | every other path | | Rendered pending flag | slow double presses across renders | same-frame events, remount | | Synchronous in-flight marker or shared promise | all client-side duplicate starts | lost response, second tab | | Request key honoured by the server | duplicate writes, whatever the cause | nothing relevant | ## How you find this in production Duplicates are usually reported as data, not as a UI bug: two identical records seconds apart, the second with the same payload. Measure it — a duplicate-write rate per endpoint — because it is the only way to know whether a fix worked. Then test the paths individually: a rapid double press, a keyboard-triggered submit, a submit followed immediately by a navigation, and a forced retry with the response dropped.

  • Why does holding the in-flight request itself work better than a boolean flag?
    Because the handler can read it synchronously and act on it: if a request for this write already exists, return it instead of starting another, so callers still get a result and nothing duplicates. It also settles in one place, which removes the class of bug where the flag and the real state of the request disagree after a throw.
  • The user submits, the route changes, and the form remounts while the request is open. What breaks?
    Any guard living in that component's state is gone, so a fresh submit is allowed, and the earlier response settles into a component that no longer exists. Move the in-flight marker to something outliving the mount — a store or a module-scoped map keyed by the record — and check the submission's identity before applying its result.
  • When is it wrong to reuse a request key?
    When the user is deliberately making a second, separate write — sending another message, creating another record with the same content. Reusing the key there makes the server return the first result and the second action silently vanishes. Generate the key when an attempt begins and retire it when the attempt settles; retries of that attempt share it, new attempts never do.

A turnstile stops the same person pushing through twice, but not a second entrance or a jammed reader — so the ticket carries a number the gate recognises when it sees it again.

saying these in an interview costs you the question

  • Treating the disabled attribute as duplicate protection
  • Setting the pending flag after an await
  • Assuming a just-written state value reads back in the handler
  • Keeping the in-flight marker in state that remounts
  • Retrying a failed write with no request key
  • Reusing one request key across separate user attempts