* feat: mount method on FormApi * fix solid-form test case * fix: added form.mount() to tests * feat: valibot-form-adapter * chore: add missing config items * docs: add Valibot React example * docs: add Solid Valibot example * docs: add valibot Vue example * fix: valibot async adapter now works * docs: add docs for valibot adapter --------- Co-authored-by: Corbin Crutchley <git@crutchcorn.dev>
10 KiB
id, title
| id | title |
|---|---|
| form-validation | Form and Field Validation |
At the core of TanStack Form's functionalities is the concept of validation. TanStack Form makes validation highly customizable:
- You can control when to perform the validation (on change, on input, on blur, on submit...)
- Validation rules can be defined at the field level or at the form level
- Validation can be synchronous or asynchronous (for example as a result of an API call)
When is validation performed?
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"
onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
>
{field => (
<>
<label htmlFor={field.name}>Age:</label>
<input
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"
onBlur={val => val < 13 ? "You must be 13 to make an account" : undefined}
>
{field => (
<>
<label htmlFor={field.name}>Age:</label>
<input
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"
onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
onBlur={(val) => (val < 0 ? "Invalid value" : undefined)}
>
{field => (
<>
<label htmlFor={field.name}>Age:</label>
<input
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.
Displaying Errors
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"
onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
>
{(field) => {
return (
<>
{/* ... */}
{field.state.meta.errors ? (
<em>{field.state.meta.errors}</em>
) : null}
</>
);
}}
</form.Field>
Or use the errorMap property to access the specific error you're looking for:
<form.Field
name="age"
onChange={val => val < 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>
Validation at field level vs at form level
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.
{/* TODO: add more details when those callbacks are fixed */}
Asynchronous Functional Validation
While we suspect most validations will be synchronous, there's 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"
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
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"
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
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>
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.
Built-in Debouncing
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}
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}
onChangeAsyncDebounceMs={1500}
onChangeAsync={async (value) => {
// ...
}}
onBlurAsync={async (value) => {
// ...
}}
children={(field) => {
return (
<>
{/* ... */}
</>
);
}}
/>
This will run
onChangeAsyncevery 1500ms whileonBlurAsyncwill run every 500ms.
Adapter-Based Validation (Zod, Yup, Valibot)
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 both 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`
validator: zodValidator,
// ...
});
<form.Field
name="age"
validator={zodValidator}
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"
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 (
<>
{/* ... */}
</>
);
}}
/>
Preventing invalid forms from being submitted
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>
)}
/>
);