Skip to content

fix(F0Form): autosubmit drops the first change on a pristine form#4695

Closed
albertpmz wants to merge 3 commits into
mainfrom
fix/f0form-autosubmit-single-change
Closed

fix(F0Form): autosubmit drops the first change on a pristine form#4695
albertpmz wants to merge 3 commits into
mainfrom
fix/f0form-autosubmit-single-change

Conversation

@albertpmz

@albertpmz albertpmz commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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) and main.

Root cause

The 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 first change saw a stale false, scheduled no timer, and was lost. A second change worked because isDirty was already true by 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 / isSubmitting guards inside the debounced setTimeout, where RHF state has settled (the timer fires ~800ms after the change):

form.watch(() => {
  if (timer) clearTimeout(timer)
  timer = setTimeout(() => {
    if (!form.formState.isDirty) return      // now reads settled state
    if (form.formState.isSubmitting) return
    submit()
  }, delay)
})

Safe:

  • form.watch never fires on mount → no spurious submit on load.
  • Not-dirty / no-interaction path still no-ops (the guard just runs later).
  • form.reset() after a successful submit triggers watch → schedules a timer → guard sees isDirty === false → no-op.
  • Debounce, rapid-change, and validation behavior unchanged.

Tests

  • New regression test: toggle a single switch on a pristine form, advance the timer, assert exactly one submit. It fails on the old code, passes on the new (verified by reverting the fix with the test in place).
  • Full F0Form suite green: 442 passed.

https://claude.ai/code/session_01Bb3Ln6Rk4Tn6mi1X2MBom4

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
Copilot AI review requested due to automatic review settings July 15, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added fix react Changes affect packages/react labels Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-4695 to install the package

Use pnpm i github:factorialco/f0#e6dcba222a2c56c85956c5db5e7ecf309a720604 to install this specific commit

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

✅ No breaking public API changes

No public exports were removed, renamed, or had existing props/types changed in a breaking way compared to main.

Comparing f0, experimental and ai against main. Adding components, types, or optional props is safe. This check is non-blocking.

⚠️ Could not analyze component-status (no-base) — a build may have failed; results may be incomplete.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 59.35% 20799 / 35043
🔵 Statements 58.39% 21810 / 37347
🔵 Functions 51.52% 4834 / 9382
🔵 Branches 52.11% 15032 / 28845
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/patterns/F0Form/F0Form.tsx 83.28% 77.94% 83.09% 84.76% 74-81, 96, 141-144, 157-162, 207-218, 354-366, 614-615, 639-649, 662, 756, 818, 819, 849, 852-863, 910, 911, 930-936, 961-962, 967-988, 992, 1185
Generated in workflow #15754 for commit 4e91b4f by the Vitest Coverage Report Action

@albertpmz
albertpmz marked this pull request as ready for review July 15, 2026 10:48
@albertpmz
albertpmz requested a review from a team as a code owner July 15, 2026 10:48
…-single-change

# Conflicts:
#	packages/react/src/patterns/F0Form/F0Form.tsx
Copilot AI review requested due to automatic review settings July 17, 2026 10:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.
Copilot AI review requested due to automatic review settings July 17, 2026 13:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@albertpmz

Copy link
Copy Markdown
Contributor Author

Closing: the core fix (reading formState.isDirty inside the debounce timer instead of synchronously in the watch callback) already landed on main via #4674 and has been shipped since @factorialco/f0-react v4.39.0. The remaining extras here (unmount guard, isSubmitting re-check, extra tests) can be resubmitted separately if wanted.

@albertpmz albertpmz closed this Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants