T
TanStack2mo ago
foreign-sapphire

What to use when the Backend returns a form error? Validator vs Submit

Hi! So I have a form that has onSubmit validators. They are there so we do basic validations (through zod) like numbers, minimum characters, etc. I also have a regular onSubmit prop where I call the backend if the validator checks that everything is okay. Now, the Backend may return errors as well, such as Username already exists, but it does so when we actually try to create the user - there isn't a separate service where I check for usernames. Basically the POST to /api/users returns a 400 with the error message. I have checked this example -> https://tanstack.com/form/latest/docs/framework/react/examples/field-errors-from-form-validators but here there isn't an onSubmit handler, just a validator. Also, I think it assumes there is a service specifically for checking if the username already exists (which isn't my case) My question is: In my case I feel like I should still call the CreateUser service inside the onSubmit because the validator shouldn't be the one creating users when the form is all okay. How would you approach this? And also, how would you highlight the username field like on the example, when there's an error OUTSIDE a validator (inside a regular onSubmit), like this?:
if (!isUsernameAvailable) {
return {
// The `form` key is optional
form: 'Invalid data',
fields: {
...(!isUsernameAvailable
? { username: 'Username is taken' }
: {}),
},
}
}
if (!isUsernameAvailable) {
return {
// The `form` key is optional
form: 'Invalid data',
fields: {
...(!isUsernameAvailable
? { username: 'Username is taken' }
: {}),
},
}
}
Thanks in advance!
React TanStack Form Field Errors From Form Validators Example | Tan...
An example showing how to implement Field Errors From Form Validators in React using TanStack Form.
42 Replies
flat-fuchsia
flat-fuchsia2mo ago
because the validator shouldn't be the one creating users when the form is all okay
That's the thing though - You have no separation of concerns here. If there are no validation errors, then the change has already happened, and if there were, then the change wasn't saved in the database. In the current implementation of onSubmit, only successful forms should go through. Only endpoints that are not validators should be called in it. If you have an endpoint that returns validation errors, you should use it in validators.onSubmitAsync. It is only called on submit, and only runs if validators.onSubmit has passed successfully. That being said - While we can talk all day about how this makes logical sense or not, it doesn't feel right. So we'll add a way to create validation errors in onSubmit sometime in the future. For now, you'll have to do with the code mentioned above.
foreign-sapphire
foreign-sapphireOP2mo ago
Thank you so much Luca! So your suggestion for now would be for me to ditch the onSubmit and instead use validators.onSubmit along with validators.onSubmitAsync, so I can react to errors, right? Thanks again! 🙏
flat-fuchsia
flat-fuchsia2mo ago
yeah. That basically sums it up How you approached it before: * Validators in validators * Validators should not mutate data * Validators return user errors * onSubmit should mutate data * onSubmit returns transient errors and user errors How TSF sees it (for now): * Validators in validators * Validators should be async, the result should determine if it had errors or not * Validators return user errors * onSubmit can mutate data * onSubmit is only for successful submissions * onSubmit returns transient errors
foreign-sapphire
foreign-sapphireOP2mo ago
Gotcha, thanks a lot, I’ll keep an eye on the news ❤️
flat-fuchsia
flat-fuchsia2mo ago
Sounds good! Let me know if you have other questions
foreign-sapphire
foreign-sapphireOP2mo ago
I actually have one more question now @Luca | LeCarbonator: This approach now seems to break the form composition pattern, when we pass the form (type errors). I have created a small StackBlitz example so you can check it, maybe I'm missing something: https://stackblitz.com/edit/validator-onsubmitasync-composable?file=src%2Findex.tsx (check line 67 on index.tsx)
seab
StackBlitz
validator.onSubmitAsync with Composable sections - StackBlitz
Run official live example code for Form Field Errors From Form Validators, created by Tanstack on StackBlitz
foreign-sapphire
foreign-sapphireOP2mo ago
For reference:
No description
flat-fuchsia
flat-fuchsia2mo ago
the error happens because withForm expects a form with a specific shape. That way, you can still get the errors in a type safe way inside withForm. However, that also means that your endpoint now caused a difference between what withForm expects and what your state will end up as so withForm expects to know what validators are present and what types they have. The actual runtime value of those validators won't matter for withForm as they are unused
foreign-sapphire
foreign-sapphireOP2mo ago
Hmm I see. So what would be the best approach here? Is it safe to ignore these errors? They always show up as soon as I use any validators.onSubmitAsync, regardless of their content
flat-fuchsia
flat-fuchsia2mo ago
the best approach would be to ensure withForm knows about the validator. In the current setup, form.state.errorMap.onSubmit is not the same type between the form in withForm vs. the form you're actually using so in this case, add this to withForm:
validators: {
onSubmitAsync: () => '' as string | null,
},
validators: {
onSubmitAsync: () => '' as string | null,
},
foreign-sapphire
foreign-sapphireOP2mo ago
That makes sense, but it doesn't seem to solve my issue :/ I'm not sure what to do next
No description
flat-fuchsia
flat-fuchsia2mo ago
oh, I thought it resolved the issue in my fork. One sec no error is thrown if I open your stackblitz. Are you sure it's not fixed?
foreign-sapphire
foreign-sapphireOP2mo ago
What browser are you using? It doesn't show any error on FF, but it does on Brave 🤔 On my local environment, on my real app, this error always occurs
flat-fuchsia
flat-fuchsia2mo ago
FF wouldn't be the first time that stackblitz has inconsistencies like that. can you copy the last few lines of the error and share them here? The ones you get on Brave
foreign-sapphire
foreign-sapphireOP2mo ago
So this is the type error I get, part 1:
Type 'AppFieldExtendedReactFormApi<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 10 more ..., {}>' is not assignable to type 'AppFieldExtendedReactFormApi<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, ... 7 more ..., {}>'.
Type 'AppFieldExtendedReactFormApi<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 10 more ..., {}>' is not assignable to type 'FormApi<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, ... 5 more ..., any>'.
The types of 'options.validators' are incompatible between these types.
Type 'FormValidators<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 6 more ..., FormAsyncValidateOrFn<...> | undefined> | undefined' is not assignable to type 'FormValidators<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, () => string | null, any, FormAsyncValidateOrFn<...> | undefined> | undefined'.
Type 'AppFieldExtendedReactFormApi<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 10 more ..., {}>' is not assignable to type 'AppFieldExtendedReactFormApi<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, ... 7 more ..., {}>'.
Type 'AppFieldExtendedReactFormApi<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 10 more ..., {}>' is not assignable to type 'FormApi<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, ... 5 more ..., any>'.
The types of 'options.validators' are incompatible between these types.
Type 'FormValidators<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 6 more ..., FormAsyncValidateOrFn<...> | undefined> | undefined' is not assignable to type 'FormValidators<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, () => string | null, any, FormAsyncValidateOrFn<...> | undefined> | undefined'.
And part 2:
Type 'FormValidators<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 6 more ..., FormAsyncValidateOrFn<...> | undefined>' is not assignable to type 'FormValidators<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, () => string | null, any, FormAsyncValidateOrFn<...> | undefined>'.
Type '({ value }: { value: { username: string; age: number; }; formApi: FormApi<{ username: string; age: number; }, any, any, any, any, any, any, any, any, any, any, any>; signal: AbortSignal; }) => Promise<...>' is not assignable to type '() => string | null'.(2322)
createFormHook.d.ts(60, 9): The expected type comes from property 'form' which is declared here on type 'IntrinsicAttributes & { form: AppFieldExtendedReactFormApi<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, ... 9 more ..., {}>; } & { ...; }'
(property) form: AppFieldExtendedReactFormApi<{
username: string;
age: number;
}, any, any, FormAsyncValidateOrFn<{
username: string;
age: number;
}> | undefined, any, FormAsyncValidateOrFn<{
username: string;
age: number;
}> | undefined, ... 7 more ..., {}>
Type 'FormValidators<{ username: string; age: number; }, FormValidateOrFn<{ username: string; age: number; }> | undefined, FormValidateOrFn<{ username: string; age: number; }> | undefined, ... 6 more ..., FormAsyncValidateOrFn<...> | undefined>' is not assignable to type 'FormValidators<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, any, () => string | null, any, FormAsyncValidateOrFn<...> | undefined>'.
Type '({ value }: { value: { username: string; age: number; }; formApi: FormApi<{ username: string; age: number; }, any, any, any, any, any, any, any, any, any, any, any>; signal: AbortSignal; }) => Promise<...>' is not assignable to type '() => string | null'.(2322)
createFormHook.d.ts(60, 9): The expected type comes from property 'form' which is declared here on type 'IntrinsicAttributes & { form: AppFieldExtendedReactFormApi<{ username: string; age: number; }, any, any, FormAsyncValidateOrFn<{ username: string; age: number; }> | undefined, ... 9 more ..., {}>; } & { ...; }'
(property) form: AppFieldExtendedReactFormApi<{
username: string;
age: number;
}, any, any, FormAsyncValidateOrFn<{
username: string;
age: number;
}> | undefined, any, FormAsyncValidateOrFn<{
username: string;
age: number;
}> | undefined, ... 7 more ..., {}>
flat-fuchsia
flat-fuchsia2mo ago
hm, looks like it's because one's async and the other one isn't
foreign-sapphire
foreign-sapphireOP2mo ago
I've also tried that, without success 🙁
flat-fuchsia
flat-fuchsia2mo ago
if only this error showed up for me :Madge: let me try chrome looks like it doesn't show up on chrome either. I'll copy the code and check locally
foreign-sapphire
foreign-sapphireOP2mo ago
I'm sorry for making you open that crap 😹 thank you so much in advance 🙏
flat-fuchsia
flat-fuchsia2mo ago
free chance to get more feedback in this case, reasons to add createError to onSubmit what the ... it's because of different argument size?
foreign-sapphire
foreign-sapphireOP2mo ago
whaaat?
flat-fuchsia
flat-fuchsia2mo ago
Okay, it looks like it's really really really really strict. More than I thought it initially was. I'll write something up in a moment, just need to check some things
flat-fuchsia
flat-fuchsia2mo ago
in the meantime, I can recommend this VSCode extension if you use it https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors
Pretty TypeScript Errors - Visual Studio Marketplace
Extension for Visual Studio Code - Make TypeScript errors prettier and more human-readable in VSCode
flat-fuchsia
flat-fuchsia2mo ago
not for this error, just in general. I really like the extension
foreign-sapphire
foreign-sapphireOP2mo ago
Oh yeah, I love it! Unfortunately StackBlitz doesn't have it 😭
flat-fuchsia
flat-fuchsia2mo ago
Yeah, unfortunately. Anyways, I was able to recreate the error. It was so specific that it even knew which fields you assigned errors to. Not that I expected it, but annoying for cases like this. If you use the latest version, there's something I can recommend instead. If you want to split up your form into multiple components (with withForm), you can extract the common fields and their types using formOptions:
const commonOptions = formOptions({
defaultValues: {
username: '',
age: 0
},
validators: {
onSubmitAsync: async ({ value }) => {
const [isRightAge, isUsernameAvailable] = await Promise.all([
// Verify the age on the server
verifyAgeOnServer(value.age),
// Verify the availability of the username on the server
checkIfUsernameIsTaken(value.username),
]);

if (!isRightAge || !isUsernameAvailable) {
return {
// The `form` key is optional
form: 'Invalid data',
fields: {
...(!isRightAge ? { age: 'Must be 13 or older to sign' } : {}),
...(!isUsernameAvailable
? { username: 'Username is taken' }
: {}),
},
};
}

return null;
}
}
})
const commonOptions = formOptions({
defaultValues: {
username: '',
age: 0
},
validators: {
onSubmitAsync: async ({ value }) => {
const [isRightAge, isUsernameAvailable] = await Promise.all([
// Verify the age on the server
verifyAgeOnServer(value.age),
// Verify the availability of the username on the server
checkIfUsernameIsTaken(value.username),
]);

if (!isRightAge || !isUsernameAvailable) {
return {
// The `form` key is optional
form: 'Invalid data',
fields: {
...(!isRightAge ? { age: 'Must be 13 or older to sign' } : {}),
...(!isUsernameAvailable
? { username: 'Username is taken' }
: {}),
},
};
}

return null;
}
}
})
What this will do is make sharing between form sections way easier.
const Fields = withForm({
...commonOptions,
// and you're set!
render: () => null
});

// main.tsx
const form = useAppForm({
...commonOptions,
// you can overwrite too if needed
defaultValues: myAsyncData ?? commonOptions.defaultValues
})
const Fields = withForm({
...commonOptions,
// and you're set!
render: () => null
});

// main.tsx
const form = useAppForm({
...commonOptions,
// you can overwrite too if needed
defaultValues: myAsyncData ?? commonOptions.defaultValues
})
This would be very convenient if you used schemas or the like, but in your case, that includes the endpoint ... The main things that withForm and useAppForm must have in common is defaultValues and validators. listeners can still be placed in useAppForm without problem -# and also maybe because they're broken in formOptions at the moment A smaller feature I've been thinking of is giving a handy function to create field-level errors from a form. Since it's not in the library (but possible), here's the snippet. Maybe it helps with type safety and broadening the type more so you encounter less of these hyper-specific errors:
function createFieldLevelError<TData, TError extends { form?: unknown; fields: Record<DeepKeys<TData>, unknown>>(value: TData, error: TError): TError {
return error;
};

// usage
createFieldLevelError(value, {
form: 'Anything',
// type-safe record
fields: { username: 'Error' }
)
function createFieldLevelError<TData, TError extends { form?: unknown; fields: Record<DeepKeys<TData>, unknown>>(value: TData, error: TError): TError {
return error;
};

// usage
createFieldLevelError(value, {
form: 'Anything',
// type-safe record
fields: { username: 'Error' }
)
foreign-sapphire
foreign-sapphireOP2mo ago
Wooow, thanks for all the information! I'd loooove this feature! How would you manually clear up these custom errors, when using that snippet?
flat-fuchsia
flat-fuchsia2mo ago
manually clear up? could you elaborate'
foreign-sapphire
foreign-sapphireOP2mo ago
Let's say I called createFieldLevelError with an error on an input field. Then the user changes the input. Does the error persist? Or would I need to call something like clearFieldLevelError on it?
flat-fuchsia
flat-fuchsia2mo ago
depends on the validator you used everything apart from onSubmit is considered persistent, so you need to have another call that clears the error in question. onSubmit errors get cleared if the field changes its value
foreign-sapphire
foreign-sapphireOP2mo ago
oh so it would work together with the onChange, onSubmit, etc handlers? that's incredible
flat-fuchsia
flat-fuchsia2mo ago
yeah, the function above works for any form-level validator and the form already knows TData, which is why it should be exported (or at least passed to the validators callback)
foreign-sapphire
foreign-sapphireOP2mo ago
Peeerfect! Thank you so much for your help ❤️
flat-fuchsia
flat-fuchsia2mo ago
no worries 👍
foreign-sapphire
foreign-sapphireOP2mo ago
Btw I had used the formOptions functions, but I thought only the defaultValues would make sense to share between form composition components. I was like: "why the hell would the <Fields /> component be aware of the onSubmit handler, or validators for other areas of the form? But from what I understood, it's not harmful to include everything, right? Even if my validators.onSubmitAsync is also present inside <Fields />
flat-fuchsia
flat-fuchsia2mo ago
well, that feature was introduced right as I started working with the library, so I don't really know why it was added I personally don't mind structured errors being returned, but to each their own yeah, in withForm, it's only for typing help no runtime stuff in withFieldGroup, the only thing that ends up being used is the Object.keys(defaultValues). It's needed so you can map form fields and the like the rest is also unused
foreign-sapphire
foreign-sapphireOP2mo ago
I see I see, I had no idea at all. It honestly seems kinda weird, but I'm glad I understood it now
flat-fuchsia
flat-fuchsia2mo ago
it's common for higher order components. But you don't see them around that much anymore
foreign-sapphire
foreign-sapphireOP2mo ago
Yup, I never seem them now. Are there any plans to change withForm from HOC to something else?
flat-fuchsia
flat-fuchsia2mo ago
hard to say. I doubt form composition will receive much of an overhaul in a v2. There's a bunch of things to address when it comes to validation though, so maybe that will indirectly affect it either way that's a long way away
foreign-sapphire
foreign-sapphireOP2mo ago
Gotcha. Again, thanks a lot and amazing work with the library, have a nice day 😄
flat-fuchsia
flat-fuchsia2mo ago
you too ❤️

Did you find this page helpful?