fix(F0Form): autosubmit drops the first change on a pristine form#4695
fix(F0Form): autosubmit drops the first change on a pristine form#4695albertpmz wants to merge 3 commits into
Conversation
The autosubmit debounce read `form.formState.isDirty` synchronously inside the `form.watch` callback. `watch` fires on the change before react-hook-form has recomputed `isDirty`, so the very first change of a pristine form saw a stale `false` and no submit was scheduled. Only a second change (by which point `isDirty` was already `true`) triggered a submit. This surfaced on single-control autosubmit forms — e.g. a lone switch toggle (performance review visibility settings): toggling one switch appeared to do nothing (no Save button, no action bar), and the change was silently lost. Fix: move the `isDirty` / `isSubmitting` guards inside the debounced `setTimeout`, where RHF state has settled. `watch` never fires on mount, so no spurious submit; the not-dirty / invalid / reset-after-submit paths still no-op. Adds a regression test that toggles a single switch on a pristine form and asserts one submit fires — it fails on the old code, passes on the new. Claude-Session: https://claude.ai/code/session_01Bb3Ln6Rk4Tn6mi1X2MBom4
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
📦 Alpha Package Version PublishedUse Use |
🔍 Visual review for your branch is published 🔍Here are the links to: |
✅ No breaking public API changesNo public exports were removed, renamed, or had existing props/types changed in a breaking way compared to Comparing
|
Coverage Report for packages/react
File Coverage
|
||||||||||||||||||||||||||||||||||||||
…-single-change # Conflicts: # packages/react/src/patterns/F0Form/F0Form.tsx
The async submit handler could resolve after the form unmounted (e.g. an in-flight autosubmit when navigating away). It then scheduled the success-message auto-dismiss timer on a dead component; because the unmount cleanup had already run, the timer was never cleared and fired a setState on a torn-down tree — surfacing in CI as an unhandled "window is not defined" from a leaked node timer. Guard the post-await path with an isMountedRef and bail. Adds a regression test asserting no timer is scheduled after unmount mid-submit, and settles the success flow in the switch-toggle test so it can't leak.
|
Closing: the core fix (reading |
Description
Fixes a bug where F0Form autosubmit silently drops the first change on a pristine form. Toggling a single switch (or any single discrete change) with
submitConfig: { type: "autosubmit" }did nothing — no submit fired — until a second change was made. Present in every released version (3.8.2, 4.34.0, 4.35.3) andmain.Root cause
The debounce read
form.formState.isDirtysynchronously inside theform.watchcallback.watchfires on the change before react-hook-form has recomputedisDirty, so the first change saw a stalefalse, scheduled no timer, and was lost. A second change worked becauseisDirtywas alreadytrueby then.User impact
Single-control autosubmit forms are the ones that break (no second change to rescue the first). Example: Performance → review process → Visibility settings — three autosubmit switches with
hideActionBar: true. An admin flips one toggle, the switch visibly moves, there's no Save button and no action bar, so it looks saved — but the mutation never fired and it reverts on reload.Screenshots (if applicable)
N/A — no visual change; behavior-only fix.
Implementation details
Moved the
isDirty/isSubmittingguards inside the debouncedsetTimeout, where RHF state has settled (the timer fires ~800ms after the change):Safe:
form.watchnever fires on mount → no spurious submit on load.form.reset()after a successful submit triggers watch → schedules a timer → guard seesisDirty === false→ no-op.Tests
F0Formsuite green: 442 passed.https://claude.ai/code/session_01Bb3Ln6Rk4Tn6mi1X2MBom4