form icon indicating copy to clipboard operation
form copied to clipboard

`canSubmit` is always `true` on first render of form

Open engelkes-finstreet opened this issue 1 year ago • 4 comments

Describe the bug

I am using a form with the zodValidator and have several field-level validations. On the first render the canSubmit property is true even though several fields are invalid. After changing the first input the validations run and canSubmit is invalid.

Your minimal, reproducible example

https://tanstack.com/form/latest/docs/framework/react/examples/zod

Steps to reproduce

  1. Go to the official zod example
  2. The Submit button is enabled even though the firstName has a minLength validation of 3
  3. Type something in any of the fields
  4. The Submit button is disabled until all the validations are correct

Expected behavior

I would expect that the canSubmit property considers all validations on the first render and shows a disabled Submit button from the start instead of changing from enabled to disabled after typing one letter and going back to enabled after all validations are correct.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

OS: MacOS Browser: Arc

TanStack Form adapter

react-form

TanStack Form version

0.20.2

TypeScript version

No response

Additional context

No response

engelkes-finstreet avatar May 29 '24 09:05 engelkes-finstreet

Hey! The current behavior in the example seems correct to me, the validator is triggered onChange hence I'd expect no validation to be run as long as the value doesn't change.

What you're trying to achieve might be validating onMount.


With that said, if we agree that onMount is the answer, I noticed that there might be a bug there preventing canSubmit to go false but it's another topic we might want to tackle (and I already have an idea why we have this bug).

An alternative option could be to manually run form.validateAllFields("change") when your form mounts.

Balastrong avatar May 29 '24 19:05 Balastrong

** I figured out the solution for canSubmit return true when your component renders,you need to validate all your form.Filed when it mounts **

function runOnMount({ value }: any) { form.validateAllFields("change"); return value; }

validators={{ onChange: (value) => { return handleMobileNumberChange(value?.value); }, onMount: runOnMount, }}

<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]} > {([canSubmit, isSubmitting]) => { return ( <Button disabled={!canSubmit} type="submit"> {!isSubmitting ? (!edit ? SUBMIT : MODIFY) : "Submiting ..."} </Button> ); }} </form.Subscribe>

afriddev avatar Jun 19 '24 05:06 afriddev

Not sure what the answer is here but I also encountered this same behaviour and find it confusing.

I validate my fields onChange (and sometimes onBlur) but I don't count the form as valid until every field has successfully ran it's validation(s).

I would've expected that disabling the "Submit" button based on isValid or canSubmit would be sufficient for this usage.

Glazy avatar Jul 06 '24 11:07 Glazy

I'm having the same issue. I'm trying to run validations at onChange (tried both at the form level and at the field level). The form is initially valid, both canSubmit and isValid are true, even though all fields are empty and thus invalid. Only when changing one of the values the form state is changed to invalid and canSubmit is false. Using onMount for the initial validation doesn't help as this only runs when the form is mounted, causing the form to stay invalid even when fields are populated and the state changes. All I want is to have the submit button disabled until the form is actually valid, which is a very simple and very common use case. I'm very surprised that it's so hard to achieve.

Hoping for a fix soon... 🙏 Thx

OPerel avatar Sep 22 '24 18:09 OPerel

Opinion: Its the fields responsibility to choose whether or not to display the error, and the field indeed should be in error state on mount. The field can choose to only display its error when the component has been touched.

<Field
    name="resource"
    validators={{
        onMount: myValidator,
        onChange: myValidator,
    }}
    children={field => (
        <MyCustomInputComponent
            //...other field props
            errors={field.state.meta.isTouched ? field.state.meta.errors : []}
        />
    )}
/>

Then just use the same validator for onChange and onMount.

The fact that you have to specify the same function for onChange and onMount is a bit annoying for sure. I have a helper function I use to simplify this for me. Just takes a function and returns the object form that tanstack is expecting.

brandonryan avatar Oct 29 '24 16:10 brandonryan

Correction: The above strategy wont work because onMount validator seems to mark the field as touched. I definitely think this is a bug. @Balastrong opinion here?

brandonryan avatar Oct 29 '24 16:10 brandonryan

Hey everyone, an update on this issue!

As mentioned at the beginning, it's correct that if there's only the onChange validator the form has canSubmit: true. Before any interaction the validator has never ran, thus the form is valid.

The recommended way is to run validation onMount too, but was broken until today as validation errors didn't go away. Now that #726 has been merged (update to version 0.34.4) you can safely use it, errors will go away as soon as a field is touched 🙌


One tiny detail remains, to have a first validation when the component mounts and keep validating when values are changed, you have to set the same validator to both onMount and onChange. Not a big deal but do you think we need a simple API to declare that and let form handle that internally? Let's discuss in #1005

Also, sorry if this took so long and thanks for all the feedback and examples!

Balastrong avatar Nov 04 '24 20:11 Balastrong

I just wanted to point out this alternative solution as well:

<form.Subscribe selector={(state) => state.isValid && !state.isPristine}>
    {(canSubmit) => <Button type="submit" disabled={!canSubmit}>Submit</Button>}
</form.Subscribe>

With that you do not need to duplicate your validator into onMount. You just don't rely on the OOTB state.canSubmit and make up your own with a selector.

johanfive avatar Feb 24 '25 01:02 johanfive

I just wanted to point out this alternative solution as well:

<form.Subscribe selector={(state) => state.isValid && !state.isPristine}> {(canSubmit) => <Button type="submit" disabled={!canSubmit}>Submit</Button>} </form.Subscribe> With that you do not need to duplicate your validator into onMount. You just don't rely on the OOTB state.canSubmit and make up your own with a selector.

With each passing hour I'm regretting some of my choices, but am now in quite deep.

In my case, I have a <select /> element that I just cannot get to show error states. Even worse, the field is unexpectedly "touched", meaning that the form can be submitted without a value in the field.

To then make things worse still, I'm seeing that despite passing a defaultValue of empty string that I might be able to validate against in useActionState, the select is taking up the value of the first <option />, which I should be able to escape according to the Radix docs. From what I gather, this is happening somewhere in the Tanstack Form context, and not the Radix context..., i.e. the value is "" at the point of submission but becomes the first option by the time tanstack hands it off.

Anyway, your solution gave me the idea to just lockdown the form so that the user can't submit it until the required fields are both touched and dirty.

  // inside my form button

<form.Subscribe
  selector={(formState) => {
    const requiredFieldsComplete = requiredFields?.map(
      (field) => formState.fieldMeta[field]?.isTouched && formState.fieldMeta[field]?.isDirty
    );

    return formState.isValid && !requiredFieldsComplete?.includes(false);
  }}
  children={(canSubmit) => (
   ...

// usage in my form...

<form.AppForm>
  <form.FormActions /* actionText={not important)} */ requiredFields={['foo', 'bar']} /> // names of fields
</form.AppForm> 

I really don't like having to do this, and would just jump ship if I wasn't staring down at a heavy deadline because some of these edges have really tested me the last few days.

akin-fagbohun avatar Apr 26 '25 12:04 akin-fagbohun

I faced the same problem on the first render. But after reading the helpful comments above and referring to the field State docs, I came up with this solution:

/**
 * Determines the form submission status based on the provided form state.
 *
 * @param state - The form state object from TanStack Form.
 * @returns An object containing:
 *   - canSubmit: A boolean indicating if the form can be submitted, which is true when the form is valid, has been modified, and can be submitted.
 *   - isSubmitting: A boolean indicating if the form is currently in the process of being submitted.
 */

const getFormSubmissionStatus = (state: AnyFormState) => ({
  canSubmit: state.isValid && !state.isPristine && state.canSubmit,
  isSubmitting: state.isSubmitting,
});

//---------------- 

<form.Subscribe
            selector={getFormSubmissionStatus}
            children={({canSubmit, isSubmitting}) => {
              return (
                <Button
                  disabled={!canSubmit}
                  loading={isSubmitting}
                >
                  text
                </Button>
              );
            }}
          />

dasezo avatar Jul 30 '25 15:07 dasezo