T
TanStack6mo ago
absent-sapphire

canSubmit true on mount

Hey everyone! I’m using TanStack Form v1 with Valibot for validation in a sign-in form. I ran into canSubmit is true on mount: My "Sign In" button uses form.Subscribe with state.canSubmit, but it’s enabled on mount even though the form is invalid (empty fields). I want it disabled until all fields pass validation.
6 Replies
fascinating-indigo
fascinating-indigo6mo ago
You need to use the onMount validator on the form to make it invalid on mount
quaint-moccasin
quaint-moccasin6mo ago
@Bilal I have a similar usecase. Can you share what you came up with? Having so many issues with onSubmit, isSubmitting, etc.
sensitive-blue
sensitive-blue6mo ago
I've tried but it didn't work. Do you need to manually do something like form.mount() before the jsx render?
fascinating-indigo
fascinating-indigo6mo ago
Nope, passing your Schema to validators.onMount should be enough. Can you share some code?
ratty-blush
ratty-blush5mo ago
Hey folks, I ran into this as well, even when using the onMount validator. After following along with the Form Composition guide: https://tanstack.com/form/latest/docs/framework/react/guides/form-composition I ended up with this:
const defaultUser: v.InferOutput<typeof SignUpSchema> = {
name: "",
email: "",
password: "",
passwordConfirm: "",
};

export const SignUp = () => {
const form = useAppForm({
defaultValues: defaultUser,
validators: {
onChange: SignUpSchema,
onMount: SignUpSchema,
},
onSubmit: ({ value }) => console.log("form submit", value),
});

return (
<form
className="flex w-3xl flex-col gap-2"
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField name="name" children={(field) => <field.TextField label="Name" />} />
<form.AppField name="email" children={(field) => <field.TextField label="Email" />} />
<form.AppField
name="password"
children={(field) => <field.TextField type="password" label="Password" />}
/>
<form.AppField
name="passwordConfirm"
children={(field) => <field.TextField type="password" label="Confirm Password" />}
/>
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</form>
);
};
const defaultUser: v.InferOutput<typeof SignUpSchema> = {
name: "",
email: "",
password: "",
passwordConfirm: "",
};

export const SignUp = () => {
const form = useAppForm({
defaultValues: defaultUser,
validators: {
onChange: SignUpSchema,
onMount: SignUpSchema,
},
onSubmit: ({ value }) => console.log("form submit", value),
});

return (
<form
className="flex w-3xl flex-col gap-2"
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.AppField name="name" children={(field) => <field.TextField label="Name" />} />
<form.AppField name="email" children={(field) => <field.TextField label="Email" />} />
<form.AppField
name="password"
children={(field) => <field.TextField type="password" label="Password" />}
/>
<form.AppField
name="passwordConfirm"
children={(field) => <field.TextField type="password" label="Confirm Password" />}
/>
<form.AppForm>
<form.SubmitButton>Submit</form.SubmitButton>
</form.AppForm>
</form>
);
};
The form doesn't really mount as invalid via my schema, but rather after mount. In the UI you can see my SubmitButton initially render as normal, and then flip to its "disabled" state. Inside the SubmitButton:
const SubmitButton = (props: ButtonProps) => {
const form = useFormContext();

const [isSubmitting, canSubmit] = useStore(form.store, (state) => [state.isSubmitting, state.canSubmit]);

console.log(canSubmit);

return <Button type="submit" disabled={isSubmitting || !canSubmit} {...props} />;
};
const SubmitButton = (props: ButtonProps) => {
const form = useFormContext();

const [isSubmitting, canSubmit] = useStore(form.store, (state) => [state.isSubmitting, state.canSubmit]);

console.log(canSubmit);

return <Button type="submit" disabled={isSubmitting || !canSubmit} {...props} />;
};
Form Composition | TanStack Form React Docs
A common criticism of TanStack Form is its verbosity out-of-the-box. While this can be useful for educational purposes helping enforce understanding our APIs it's not ideal in production use cases. As...
ratty-blush
ratty-blush5mo ago
The log prints twice:
true
false
true
false
So Idk if that means I'm rendering it incorrectly, or if I need to provide better defaults, but preventing that UI flash without me manually going in and adding some additional state or something would be swell. OH this is absolutely an ssr thing. Disabling SSR on this page makes the mount behavior work just fine Ok so for my usecase, I think I need to just initalize the component with the state that I want it in, then let the form state do its thing after hydration. For any future lurkers, I opted to just allow disabling parts of the UI until after hydration:
const SubmitButton = ({ disableUntilMount, ...props }: ButtonProps & { disableUntilMount?: boolean }) => {
const form = useFormContext();
const isMounted = useIsMounted();

const [isSubmitting, canSubmit] = useStore(form.store, (state) => [state.isSubmitting, state.canSubmit]);

return (
<Button
type="submit"
disabled={isSubmitting || !canSubmit || (disableUntilMount && !isMounted())}
{...props}
/>
);
};
const SubmitButton = ({ disableUntilMount, ...props }: ButtonProps & { disableUntilMount?: boolean }) => {
const form = useFormContext();
const isMounted = useIsMounted();

const [isSubmitting, canSubmit] = useStore(form.store, (state) => [state.isSubmitting, state.canSubmit]);

return (
<Button
type="submit"
disabled={isSubmitting || !canSubmit || (disableUntilMount && !isMounted())}
{...props}
/>
);
};

Did you find this page helpful?