At the core of TanStack Form's functionalities is the concept of validation. TanStack Form makes validation highly customizable:
It's up to you! The <Field />
component accepts some callbacks as props such as onChange
or onBlur
. Those callbacks are passed the current value of the field, as well as the fieldAPI object, so that you can perform the validation. If you find a validation error, simply return the error message as string and it will be available in field.state.meta.errors
.
Here is an example:
<form.Field name="age" validators={{ onChange: ({value}) => value < 13 ? 'You must be 13 to make an account' : undefined, }}> {(field) => ( <> <label htmlFor={field.name}>Age:</label> <input id={field.name} name={field.name} value={field.state.value} type="number" onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors ? ( <em role="alert">{field.state.meta.errors.join(', ')}</em> ) : null} </> )}</form.Field>
In the example above, the validation is done at each keystroke (onChange
). If, instead, we wanted the validation to be done when the field is blurred, we would change the code above like so:
<form.Field name="age" validators={{ onBlur: ({value}) => value < 13 ? 'You must be 13 to make an account' : undefined, }}> {(field) => ( <> <label htmlFor={field.name}>Age:</label> <input id={field.name} name={field.name} value={field.state.value} type="number" // Listen to the onBlur event on the field onBlur={field.handleBlur} // We always need to implement onChange, so that TanStack Form receives the changes onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors ? ( <em role="alert">{field.state.meta.errors.join(', ')}</em> ) : null} </> )}</form.Field>
So you can control when the validation is done by implementing the desired callback. You can even perform different pieces of validation at different times:
<form.Field name="age" validators={{ onChange: ({value}) => value < 13 ? 'You must be 13 to make an account' : undefined, onBlur: ({value}) => (value < 0 ? 'Invalid value' : undefined), }}> {(field) => ( <> <label htmlFor={field.name}>Age:</label> <input id={field.name} name={field.name} value={field.state.value} type="number" // Listen to the onBlur event on the field onBlur={field.handleBlur} // We always need to implement onChange, so that TanStack Form receives the changes onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors ? ( <em role="alert">{field.state.meta.errors.join(', ')}</em> ) : null} </> )}</form.Field>
In the example above, we are validating different things on the same field at different times (at each keystroke and when blurring the field). Since field.state.meta.errors
is an array, all the relevant errors at a given time are displayed. You can also use field.state.meta.errorMap
to get errors based on when the validation was done (onChange, onBlur etc...). More info about displaying errors below.
Once you have your validation in place, you can map the errors from an array to be displayed in your UI:
<form.Field name="age" validators={{ onChange: ({value}) => value < 13 ? 'You must be 13 to make an account' : undefined, }}> {(field) => { return ( <> {/* ... */} {field.state.meta.errors.length ? <em>{field.state.meta.errors.join(",")}</em> : null} </> ) }}</form.Field>
Or use the errorMap
property to access the specific error you're looking for:
<form.Field name="age" validators={{ onChange: ({value}) => value < 13 ? 'You must be 13 to make an account' : undefined, }}> {(field) => ( <> {/* ... */} {field.state.meta.errorMap['onChange'] ? ( <em>{field.state.meta.errorMap['onChange']}</em> ) : null} </> )}</form.Field>
As shown above, each <Field>
accepts its own validation rules via the onChange
, onBlur
etc... callbacks. It is also possible to define validation rules at the form level (as opposed to field by field) by passing similar callbacks to the useForm()
hook.
Example:
export default function App() { const form = useForm({ defaultValues: { age: 0, }, onSubmit: async ({ value }) => { console.log(value) }, validators: { // Add validators to the form the same way you would add them to a field onChange({ value }) { if (value.age < 13) { return 'Must be 13 or older to sign' } return undefined }, }, })
// Subscribe to the form's error map so that updates to it will render // alternately, you can use `form.Subscribe` const formErrorMap = form.useStore((state) => state.errorMap)
return ( <div> {/* ... */} {formErrorMap.onChange ? ( <div> <em>There was an error on the form: {formErrorMap.onChange}</em> </div> ) : null} {/* ... */} </div> )}
You can set errors on the fields from the form's validators. One common use case for this is validating all the fields on submit by calling a single API endpoint in the form's onSubmitAsync
validator.
export default function App() { const form = useForm({ defaultValues: { age: 0, }, validators: { onSubmitAsync: async ({ value }) => { // Verify the age on the server const isOlderThan13 = await verifyAgeOnServer(value.age) if (!isOlderThan13) { return { form: 'Invalid data', // The `form` key is optional fields: { age: 'Must be 13 or older to sign', }, } }
return null }, }, })
return ( <div> <form onSubmit={(e) => { e.preventDefault() e.stopPropagation() void form.handleSubmit() }} > <form.Field name="age"> {(field) => ( <> <label htmlFor={field.name}>Age:</label> <input id={field.name} name={field.name} value={field.state.value} type="number" onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors ? ( <em role="alert">{field.state.meta.errors.join(', ')}</em> ) : null} </> )} </form.Field> <form.Subscribe selector={(state) => [state.errorMap]} children={([errorMap]) => errorMap.onSubmit ? ( <div> <em>There was an error on the form: {errorMap.onSubmit}</em> </div> ) : null } /> {/*...*/} </form> </div> )}
Something worth mentioning is that if you have a form validation function that returns an error, that error may be overwritten by the field-specific validation.
This means that:
jsx
const form = useForm({defaultValues: {age: 0,},validators: {onChange: ({ value }) => {return {fields: {age: value.age < 12 ? 'Too young!' : undefined,},}},},})// ...return <form.Fieldname="age"validators={{onChange: ({ value }) => value % 2 === 0 ? 'Must be odd!' : undefined,}}/>Will only show
'Must be odd!
even if the 'Too young!' error is returned by the form-level validation.
While we suspect most validations will be synchronous, there are many instances where a network call or some other async operation would be useful to validate against.
To do this, we have dedicated onChangeAsync
, onBlurAsync
, and other methods that can be used to validate against:
<form.Field name="age" validators={{ onChangeAsync: async ({value}) => { await new Promise((resolve) => setTimeout(resolve, 1000)) return value < 13 ? 'You must be 13 to make an account' : undefined }, }}> {(field) => ( <> <label htmlFor={field.name}>Age:</label> <input id={field.name} name={field.name} value={field.state.value} type="number" onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors ? ( <em role="alert">{field.state.meta.errors.join(', ')}</em> ) : null} </> )}</form.Field>
Synchronous and Asynchronous validations can coexist. For example, it is possible to define both onBlur
and onBlurAsync
on the same field:
<form.Field name="age" validators={{ onBlur: ({value}) => (value < 13 ? 'You must be at least 13' : undefined), onBlurAsync: async ({value}) => { const currentAge = await fetchCurrentAgeOnProfile() return value < currentAge ? 'You can only increase the age' : undefined }, }}> {(field) => ( <> <label htmlFor={field.name}>Age:</label> <input id={field.name} name={field.name} value={field.state.value} type="number" onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors ? ( <em role="alert">{field.state.meta.errors.join(', ')}</em> ) : null} </> )}</form.Field>
The synchronous validation method (onBlur
) is run first and the asynchronous method (onBlurAsync
) is only run if the synchronous one (onBlur
) succeeds. To change this behaviour, set the asyncAlways
option to true
, and the async method will be run regardless of the result of the sync method.
While async calls are the way to go when validating against the database, running a network request on every keystroke is a good way to DDOS your database.
Instead, we enable an easy method for debouncing your async
calls by adding a single property:
<form.Field name="age" asyncDebounceMs={500} validators={{ onChangeAsync: async ({value}) => { // ... } }} children={(field) => { return ( <> {/* ... */} </> ); }}/>
This will debounce every async call with a 500ms delay. You can even override this property on a per-validation property:
<form.Field name="age" asyncDebounceMs={500} validators={{ onChangeAsyncDebounceMs: 1500, onChangeAsync: async ({value}) => { // ... }, onBlurAsync: async ({value}) => { // ... }, }} children={(field) => { return <>{/* ... */}</> }}/>
This will run
onChangeAsync
every 1500ms whileonBlurAsync
will run every 500ms.
While functions provide more flexibility and customization over your validation, they can be a bit verbose. To help solve this, there are libraries like Valibot, Yup, and Zod that provide schema-based validation to make shorthand and type-strict validation substantially easier.
Luckily, we support all of these libraries through official adapters:
$ npm install @tanstack/zod-form-adapter zod# or$ npm install @tanstack/yup-form-adapter yup# or$ npm install @tanstack/valibot-form-adapter valibot
Once done, we can add the adapter to the validator
property on the form or field:
import { zodValidator } from '@tanstack/zod-form-adapter'import { z } from 'zod'
// ...
const form = useForm({ // Either add the validator here or on `Field` validatorAdapter: zodValidator(), // ...})
<form.Field name="age" validatorAdapter={zodValidator()} validators={{ onChange: z.number().gte(13, 'You must be 13 to make an account'), }} children={(field) => { return <>{/* ... */}</> }}/>
These adapters also support async operations using the proper property names:
<form.Field name="age" validators={{ onChange: z.number().gte(13, 'You must be 13 to make an account'), onChangeAsyncDebounceMs: 500, onChangeAsync: z.number().refine( async (value) => { const currentAge = await fetchCurrentAgeOnProfile() return value >= currentAge }, { message: 'You can only increase the age', }, ), }} children={(field) => { return <>{/* ... */}</> }}/>
You can also use the adapter at the form level:
import { zodValidator } from '@tanstack/zod-form-adapter'import { z } from 'zod'
// ...
const formSchema = z.object({ age: z.number().gte(13, 'You must be 13 to make an account'),})
const form = useForm({ validatorAdapter: zodValidator(), validators: { onChange: formSchema },})
If you use the adapter at the form level, it will pass the validation to the fields of the same name.
This means that:
<form.Field name="age" children={(field) => { return <>{/* ... */}</> }}/>
Will still display the error message from the form-level validation.
The onChange
, onBlur
etc... callbacks are also run when the form is submitted and the submission is blocked if the form is invalid.
The form state object has a canSubmit
flag that is false when any field is invalid and the form has been touched (canSubmit
is true until the form has been touched, even if some fields are "technically" invalid based on their onChange
/onBlur
props).
You can subscribe to it via form.Subscribe
and use the value in order to, for example, disable the submit button when the form is invalid (in practice, disabled buttons are not accessible, use aria-disabled
instead).
const form = useForm(/* ... */)
return ( /* ... */
// Dynamic submit button <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]} children={([canSubmit, isSubmitting]) => ( <button type="submit" disabled={!canSubmit}> {isSubmitting ? '...' : 'Submit'} </button> )} />)
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.