`canSubmit` is always `true` on first render of form
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
- Go to the official zod example
- The
Submitbutton is enabled even though the firstName has a minLength validation of 3 - Type something in any of the fields
- The
Submitbutton 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
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.
** 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>
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.
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
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.
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?
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!
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.
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 OOTBstate.canSubmitand make up your own with aselector.
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.
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>
);
}}
/>