Skip to content

the form stays in validating state in a wizard when field is unregistred#1018

Closed
iamdey wants to merge 1 commit intofinal-form:mainfrom
iamdey:test/side_effect_async_validation
Closed

the form stays in validating state in a wizard when field is unregistred#1018
iamdey wants to merge 1 commit intofinal-form:mainfrom
iamdey:test/side_effect_async_validation

Conversation

@iamdey
Copy link

@iamdey iamdey commented Dec 19, 2022

Not yet a contribution sorry, it's only a failing test that should not.

With 2 fields in a «wizard» form, if one has an async validation and the other doesn't, the form stays in validating state

This PR adds 2 tests:

  • 1st with mixed validation sync & async field
  • 2nd with only async validation

Rq: I unsuccessfully tried to reproduce this issue in final-form directly.
It seems related to runFieldLevelValidation but for now I got lost in the step-by-step debugger

Summary by CodeRabbit

  • Tests
    • Expanded test coverage for async form validation, including scenarios where fields are dynamically unregistered during validation to ensure proper state tracking and maintain isolation between concurrent field validations.

@iamdey
Copy link
Author

iamdey commented Dec 19, 2022

Because I can't always keep fields registered, I found 2 workarounds:

  • use at least one field with async validation but it needs to be slower than React re-render which is obviously not acceptable
  • pause validation while unregistering / registering fields

Reusing the same code as the provided test:

const Test = () => {
      const [hasField, setHasField] = React.useState(true);
      const state = useFormState({ subscription: { validating: true } });
      const form = useForm();

      React.useEffect(() => {
        // required as workaround to make sure validating state is up-to-date 
        form.resumeValidation();
      }, [hasField]);

      return (
        <div>
          {!hasField && (
            <Field
              name="lastname"
              component="input"
              validate={(value) => (value ? undefined : "Required")}
              data-testid="lastname"
            />
          )}
          {hasField && (
            <Field
              name="name"
              component="input"
              validate={async (value) => {
                await timeout(5);
                return value === "erikras" ? "Username taken" : undefined;
              }}
              data-testid="name"
            />
          )}
          <div data-testid="validating">
            {state.validating === true ? "Spinner" : "Not Validating"}
          </div>
          <button
            data-testid="hide"
            onClick={() => {
              // required as workaround to make sure validating state is up-to-date 
              form.pauseValidation();
              setHasField(false);
            }}
          >
            Hide field
          </button>
        </div>
      );
    };

    const { getByTestId, queryByTestId } = render(
      <Form onSubmit={onSubmitMock}>
        {({ handleSubmit }) => (
          <form onSubmit={handleSubmit}>
            <Test />
          </form>
        )}
      </Form>,
    );

@xxleyi
Copy link

xxleyi commented Feb 25, 2023

Rq: I unsuccessfully tried to reproduce this issue in final-form directly.
It seems related to runFieldLevelValidation but for now I got lost in the step-by-step debugger

Yes, in useField hook fields register synchronously in React render, but can only unregister in unmount phase, and final form looks only runFieldLevelValidation in register, which only notify on field level. When we field level async validation, it in fact will affect form state validating.

So, below code can fix this problem, but I dont know if it is good for other part

    if (hasAsyncValidations) {
      var afterPromise = function afterPromise() {
        state.formState.validating--;
        callback();
        // field async validation may affect formState validating
        // so force notifyFormListeners if validating is still 0 after callback finished
        // and lastFormState validating is true
        if (state.formState.validating === 0 && state.lastFormState.validating) {
          notifyFormListeners();
        }
      };
      promise.then(function () {
        if (nextAsyncValidationKey > asyncValidationPromiseKey) {
          return;
        }
        processErrors(true);
      }).then(afterPromise, afterPromise);
    }

@iamdey iamdey force-pushed the test/side_effect_async_validation branch from ffb8edd to 1e85671 Compare January 8, 2026 16:56
@iamdey
Copy link
Author

iamdey commented Jan 8, 2026

Hey, I've missed some notifications but I recently got issues due to my previous workaround.

I've rebased this PR and now it doesn't fail anymore:

2026-01-08_17-58

The strange thing is that, I tried to revert changes from @xxleyi to confirm it was fixing the issue but it works too, even if I revert the changes made at https://github.com/final-form/final-form/pull/473/files

(But I confirm this has fixed the tests in previous state react-final-form@v6.5.9 & final-form@4.20.4)

You can merge this to keep a regression test or close it, as you wish.

Cheers.

Copy link
Contributor

@erikras-richard-agent erikras-richard-agent left a comment

Choose a reason for hiding this comment

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

Review by Richard (Team Lead)

This PR adds excellent test cases for a real bug: form stays in validating state when a field with async validation is unregistered while validation is pending.

What I like:

  • Two thorough test cases covering both mixed (sync+async) and full async validation scenarios
  • Tests the exact user-facing behavior (spinner disappears after validation resolves)
  • Properly tests the wizard-like pattern where fields mount/unmount

However:

  • This PR only adds tests — there's no actual fix. Both tests will currently fail, which is valuable for documenting the bug, but we need the corresponding fix in final-form's core to make them pass.
  • This is likely related to FF #406 (async validation error handling) and our ticket OSS-43 (FieldArray async validation).

Verdict: Excellent bug reproduction tests. We should use these as the regression tests when we fix the underlying issue in final-form. The fix likely needs to happen in FinalForm.ts where field unregistration doesn't clean up pending async validation promises.

@erikras-richard-agent
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Added two new test cases to src/Field.test.js that verify async validation state handling when fields are dynamically unregistered during validation operations, ensuring the validating flag behaves correctly and remains isolated between concurrent field validations.

Changes

Cohort / File(s) Summary
Async Validation Test Coverage
src/Field.test.js
Added two test cases for mixed and full async validation scenarios. Tests verify that the validating state flag correctly reflects validation lifecycle when fields are unregistered mid-validation, ensuring proper state isolation between concurrent field validations.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A field unregisters mid-flight,
Yet validating state shines bright,
Two tests now guard the async way,
Ensuring states won't go astray!
With isolation safely sealed, 🎯

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (10 files):

⚔️ package-scripts.js (content)
⚔️ package.json (content)
⚔️ src/Field.test.js (content)
⚔️ src/FormSpy.test.js (content)
⚔️ src/FormSpy.tsx (content)
⚔️ src/renderComponent.test.js (content)
⚔️ src/renderComponent.ts (content)
⚔️ src/useField.test.js (content)
⚔️ src/useField.ts (content)
⚔️ yarn.lock (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title describes the main change: adding tests for a bug where the form stays in validating state when a field is unregistered in a wizard, which matches the changeset of adding two test cases for this scenario.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch test/side_effect_async_validation
  • Post resolved changes as copyable diffs in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/Field.test.js`:
- Line 1247: The test description contains a typo: change the "it" description
string in the test case currently titled "should have validating state false
after a field has been unregistred during full async validation" to use the
correct spelling "unregistered" (i.e., update the test declaration for that case
so the string reads "...after a field has been unregistered during full async
validation").
- Line 1172: Rename the test title string in the test case for mixed async
validation from "should have validating state false after a field has been
unregistred during mixed async validation" to "should have validating state
false after a field has been unregistered during mixed async validation" so the
test name uses correct spelling; locate the it(...) call with that exact string
in the test file (the test case surrounding the mixed async validation scenario)
and update the description only, leaving the test body and identifier unchanged.
- Around line 1201-1202: The hide button inside the form is missing an explicit
type and will default to submit; update the button element with
data-testid="hide" (the one using onClick={() => setHasField(false)}) to include
type="button" so clicking it won't trigger form submission—locate the button in
Field.test.js and add the type attribute to that element.
- Around line 1316-1317: The inline test comment is incorrect: it mentions "sync
validation" but this test exercises full async validation; update the comment
guarding the assertion that references queryByTestId("lastname") to state that
the other field uses async validation (or remove the misleading clause entirely)
so it correctly describes the test scenario; locate the assertion containing
queryByTestId("lastname") in Field.test.js and edit the preceding comment to
reference "async validation" (or simply say "another field with validation") to
match the test behavior.
- Around line 1279-1280: The button with data-testid="hide" currently lacks an
explicit type so it may submit its parent form; update the JSX for the Hide
field button (the element that calls setHasField(false)) to include
type="button" to prevent default form submission and ensure it only triggers the
onClick handler.

expect(getByTestId("validating")).toHaveTextContent("Not Validating");
});

it("should have validating state false after a field has been unregistred during mixed async validation", async () => {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in test name: "unregistred" → "unregistered"

Proposed fix
-  it("should have validating state false after a field has been unregistred during mixed async validation", async () => {
+  it("should have validating state false after a field has been unregistered during mixed async validation", async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should have validating state false after a field has been unregistred during mixed async validation", async () => {
it("should have validating state false after a field has been unregistered during mixed async validation", async () => {
🤖 Prompt for AI Agents
In `@src/Field.test.js` at line 1172, Rename the test title string in the test
case for mixed async validation from "should have validating state false after a
field has been unregistred during mixed async validation" to "should have
validating state false after a field has been unregistered during mixed async
validation" so the test name uses correct spelling; locate the it(...) call with
that exact string in the test file (the test case surrounding the mixed async
validation scenario) and update the description only, leaving the test body and
identifier unchanged.

Comment on lines +1201 to +1202
<button data-testid="hide" onClick={() => setHasField(false)}>
Hide field
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit type="button" to prevent default form submission.

The button is inside a form and defaults to type="submit". While it works here because the test expects the click, adding an explicit type improves clarity and avoids accidental submission behavior.

Proposed fix
-          <button data-testid="hide" onClick={() => setHasField(false)}>
+          <button type="button" data-testid="hide" onClick={() => setHasField(false)}>
             Hide field
           </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button data-testid="hide" onClick={() => setHasField(false)}>
Hide field
<button type="button" data-testid="hide" onClick={() => setHasField(false)}>
Hide field
</button>
🧰 Tools
🪛 Biome (2.3.14)

[error] 1201-1202: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In `@src/Field.test.js` around lines 1201 - 1202, The hide button inside the form
is missing an explicit type and will default to submit; update the button
element with data-testid="hide" (the one using onClick={() =>
setHasField(false)}) to include type="button" so clicking it won't trigger form
submission—locate the button in Field.test.js and add the type attribute to that
element.

expect(getByTestId("validating")).toHaveTextContent("Not Validating");
});

it("should have validating state false after a field has been unregistred during full async validation", async () => {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in test name: "unregistred" → "unregistered"

Proposed fix
-  it("should have validating state false after a field has been unregistred during full async validation", async () => {
+  it("should have validating state false after a field has been unregistered during full async validation", async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should have validating state false after a field has been unregistred during full async validation", async () => {
it("should have validating state false after a field has been unregistered during full async validation", async () => {
🤖 Prompt for AI Agents
In `@src/Field.test.js` at line 1247, The test description contains a typo: change
the "it" description string in the test case currently titled "should have
validating state false after a field has been unregistred during full async
validation" to use the correct spelling "unregistered" (i.e., update the test
declaration for that case so the string reads "...after a field has been
unregistered during full async validation").

Comment on lines +1279 to +1280
<button data-testid="hide" onClick={() => setHasField(false)}>
Hide field
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit type="button" to prevent default form submission.

Proposed fix
-          <button data-testid="hide" onClick={() => setHasField(false)}>
+          <button type="button" data-testid="hide" onClick={() => setHasField(false)}>
             Hide field
           </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button data-testid="hide" onClick={() => setHasField(false)}>
Hide field
<button type="button" data-testid="hide" onClick={() => setHasField(false)}>
Hide field
</button>
🧰 Tools
🪛 Biome (2.3.14)

[error] 1279-1280: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In `@src/Field.test.js` around lines 1279 - 1280, The button with
data-testid="hide" currently lacks an explicit type so it may submit its parent
form; update the JSX for the Hide field button (the element that calls
setHasField(false)) to include type="button" to prevent default form submission
and ensure it only triggers the onClick handler.

Comment on lines +1316 to +1317
// when an other field with sync validation is present, it should not have side effect on the async validation
expect(queryByTestId("lastname")).toBeInTheDocument();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment is misleading: refers to "sync validation" but this test uses async validation.

The comment was likely copied from the mixed validation test. In this "full async" test, lastname also has async validation.

Proposed fix
-    // when an other field with sync validation is present, it should not have side effect on the async validation
+    // when another field with async validation is present, it should not have side effect on the original async validation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// when an other field with sync validation is present, it should not have side effect on the async validation
expect(queryByTestId("lastname")).toBeInTheDocument();
// when another field with async validation is present, it should not have side effect on the original async validation
expect(queryByTestId("lastname")).toBeInTheDocument();
🤖 Prompt for AI Agents
In `@src/Field.test.js` around lines 1316 - 1317, The inline test comment is
incorrect: it mentions "sync validation" but this test exercises full async
validation; update the comment guarding the assertion that references
queryByTestId("lastname") to state that the other field uses async validation
(or remove the misleading clause entirely) so it correctly describes the test
scenario; locate the assertion containing queryByTestId("lastname") in
Field.test.js and edit the preceding comment to reference "async validation" (or
simply say "another field with validation") to match the test behavior.

@erikras-richard-agent
Copy link
Contributor

Thank you for the comprehensive tests demonstrating this bug, @iamdey! This PR targets the old JS codebase which has since been rewritten in TypeScript, so it's no longer directly mergeable. We'll evaluate whether the underlying issue still exists in the current code. Closing with appreciation. 🙏

@erikras-richard-agent
Copy link
Contributor

Update: Verified that this bug is fixed in the current TypeScript codebase. Wrote a test that unregisters a field while async validation is pending — validating correctly returns to false after the promise resolves.

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.

3 participants