Skip to content

fix(ui): prevent concurrent form submissions on double-click [#2982]#2983

Merged
viniciusdacal merged 1 commit intomainfrom
fix/form-concurrent-submission
Apr 23, 2026
Merged

fix(ui): prevent concurrent form submissions on double-click [#2982]#2983
viniciusdacal merged 1 commit intomainfrom
fix/form-concurrent-submission

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

  • form() didn't lock against re-entrant submissions — double-clicking submit fired two submit events, both entered submitPipeline, passed validation, and called the SDK, creating duplicate records.
  • submitPipeline now checks submitting.peek() synchronously at entry and bails if already in flight; submitting.value = true is set before any work; a try/finally resets it on every exit path.
  • Pipeline returns a boolean so onSubmit / submit wrappers skip their post-processing (form reset) when their call was rejected — prevents a stale double formElement.reset() on the losing click.

Closes #2982

Public API Changes

None. Internal refactor of submitPipeline. User-facing contract is unchanged — just fewer bugs.

Test plan

  • packages/ui/src/form/__tests__/form-concurrent.test.ts — 4 new tests:
    • Re-entrant submit() calls — SDK called once, submitting settles back to false.
    • Sequential submits still work (lock releases).
    • Double-clicked onSubmit — SDK called once, onSuccess fired once, only the winning event's target.reset() is invoked.
    • Validation failure on the same form releases the lock so the next submission can proceed.
  • All 2468 @vertz/ui tests pass.
  • vtz run typecheck green.
  • vtzx oxlint / vtzx oxfmt clean on changed files.

Review

Adversarial self-review at reviews/fix-2982/review.md (not committed — working artifact). Two blockers found and addressed: (1) outer wrappers would still reset the form on rejected calls, fixed by returning a boolean from the pipeline and gating post-processing; (2) validation-failure test used two different form instances so it didn't exercise the same-form lock release — rewrote to use a single form with a schema that flips between fail/pass.

🤖 Generated with Claude Code

Double-clicking a submit button fired two submit events back-to-back. Both
entered submitPipeline, passed validation, and called the SDK — creating
duplicate records. submitting was only set to true after validation, and
there was no reentrancy guard.

submitPipeline now checks submitting.peek() synchronously at entry and
returns early if a submission is already in flight. submitting.value = true
is set before any work (so a second sync call sees the guard) and a
try/finally guarantees it is reset on every exit path. The pipeline returns
a boolean so onSubmit / submit wrappers skip their post-processing (form
reset) when their call was rejected.

Closes #2982

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@viniciusdacal viniciusdacal merged commit 513fe1e into main Apr 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

form() should prevent concurrent submissions (double-click creates duplicates)

1 participant