diff --git a/docs/guides/validation.md b/docs/guides/validation.md index 621cca2e2..7139886a9 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -3,43 +3,92 @@ id: form-validation title: Form and Field Validation --- -At the core of TanStack Form's functionalities is the concept of validation. We currently support three mechanisms of 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) -- Synchronous functional validation -- Asynchronous functional validation -- Adapter-based validation -Let's take a look at each and see how they're built. +## When is validation performed? -## Synchronous Functional Validation +It's up to you! The `` 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`. -With Form, you can pass a function to a field and, if it returns a string, said string will be used as the error: +Here is an example: ```tsx val < 13 ? "You must be 13 to make an account" : undefined} - children={(field) => { - return ( - <> - - field.handleChange(e.target.valueAsNumber)} - /> - {field.state.meta.touchedErrors ? ( - {field.state.meta.touchedErrors} - ) : null} - - ); - }} -/> +> + {field => ( + <> + + field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors ? {field.state.meta.errors.join(', ')} : null} + + )} + ``` -### Displaying Errors +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: + +```tsx + val < 13 ? "You must be 13 to make an account" : undefined} +> + {field => ( + <> + + field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors ? {field.state.meta.errors.join(', ')} : null} + + )} + +``` + +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: + +```tsx + val < 13 ? "You must be 13 to make an account" : undefined} + onBlur={(val) => (val < 0 ? "Invalid value" : undefined)} +> + {field => ( + <> + + field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors ? {field.state.meta.errors.join(', ')} : null} + + )} + +``` + +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: @@ -47,7 +96,8 @@ Once you have your validation in place, you can map the errors from an array to val < 13 ? "You must be 13 to make an account" : undefined} - children={(field) => { +> + {(field) => { return ( <> {/* ... */} @@ -57,7 +107,7 @@ Once you have your validation in place, you can map the errors from an array to ); }} -/> + ``` Or use the `errorMap` property to access the specific error you're looking for: @@ -66,36 +116,24 @@ Or use the `errorMap` property to access the specific error you're looking for: val < 13 ? "You must be 13 to make an account" : undefined} - children={(field) => { - return ( +> + {(field) => ( <> {/* ... */} {field.state.meta.errorMap['onChange'] ? ( {field.state.meta.errorMap['onChange']} ) : null} - ); - }} -/> + )} + ``` -### Using Alternative Validation Steps +## Validation at field level vs at form level -One of the great benefits of using TanStack Form is that you're not locked into a specific method of validation. For example, if you want to validate a specific field on blur rather than on text change, you can change `onChange` to `onBlur`: +As shown above, each `` 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. + + -```tsx - val < 13 ? "You must be 13 to make an account" : undefined} - children={(field) => { - return ( - <> - {/* ... */} - - ); - }} -/> -``` ## Asynchronous Functional Validation @@ -105,58 +143,60 @@ To do this, we have dedicated `onChangeAsync`, `onBlurAsync`, and other methods ```tsx { await new Promise((resolve) => setTimeout(resolve, 1000)); return ( - value.includes("error") && 'No "error" allowed in first name' + value < 13 ? "You must be 13 to make an account" : undefined ); }} - children={(field) => { - return ( - <> - - field.handleChange(e.target.value)} - /> - - - ); - }} -/> +> + {field => ( + <> + + field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors ? {field.state.meta.errors.join(', ')} : null} + + )} + ``` -This can be combined with the respective synchronous properties as well: +Synchronous and Asynchronous validations can coexist. For example it is possible to define both `onBlur` and `onBlurAsync` on the same field: -``` tsx +```tsx - !value - ? "A first name is required" - : value.length < 3 - ? "First name must be at least 3 characters" - : undefined - } - onChangeAsync={async (value) => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - return ( - value.includes("error") && 'No "error" allowed in first name' - ); - }} - children={(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 => ( + <> + + field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors ? {field.state.meta.errors.join(', ')} : null} + + )} + ``` +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. @@ -165,7 +205,7 @@ Instead, we enable an easy method for debouncing your `async` calls by adding a ```tsx { // ... @@ -184,7 +224,7 @@ This will debounce every async call with a 500ms delay. You can even override th ```tsx { @@ -206,7 +246,7 @@ This will debounce every async call with a 500ms delay. You can even override th > This will run `onChangeAsync` every 1500ms while `onBlurAsync` will run every 500ms. -## Adapter-Based Validation +## Adapter-Based Validation (Zod, Yup) While functions provide more flexibility and customization over your validation, they can be a bit verbose. To help solve this, there are libraries like [Yup](https://github.com/jquense/yup) and [Zod](https://zod.dev/) that provide schema-based validation to make shorthand and type-strict validation substantially easier. @@ -233,11 +273,11 @@ const form = useForm({ }); { return ( <> @@ -252,18 +292,20 @@ These adapters also support async operations using the proper property names: ```tsx { - await new Promise((resolve) => setTimeout(resolve, 1000)); - return !value.includes("error"); + const currentAge = await fetchCurrentAgeOnProfile(); + return ( + value >= currentAge + ); }, { - message: "No 'error' allowed in first name", + message: "You can only increase the age", }, )} children={(field) => { @@ -276,3 +318,28 @@ These adapters also support async operations using the proper property names: /> ``` +## 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). + +```tsx +const form = useForm(/* ... */); + +return ( + /* ... */ + + // Dynamic submit button + [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> +); +```