Files
form/docs/guides/validation.md
Corbin Crutchley 54652ee674 feat: Add Yup and Zod validator support (#462)
* chore: first pass

* chore: onto something I think

* chore: closer but no cigar

* chore: infer validator

* chore: infer zod

* feat: add validation transformer logic

* chore: fix typings for react adapter

* chore: fix issue with `this` not being defined properly

* chore: mostly update FieldInfo types from Vue

* chore: work on fixing type inferencing

* fix: make ValidatorType optional

* chore: make TName restriction easier to grok

* chore: fix React types

* chore: fix Vue types

* chore: fix typing issues

* chore: fix various linting items

* chore: fix ESlint and validation logic

* chore: fix inferencing from formdata

* chore: fix form inferencing

* chore: fix React TS types to match form validator logic

* chore: fix Vue types

* chore: migrate zod validation to dedicated package

* chore: add first integration test for zod adapter

* chore: enable non-validator types to be passed to validator

* feat: add yup 1.x adapter

* chore: add functionality and tests for form-wide validators

* chore: fix typings of async validation types

* fix: async validation should now run as-expected more often

* chore: add async tests for Yup validator

* chore: rename packages to match naming schema better

* chore: add Zod examples for React and Vue

* chore: add React and Vue Yup support

* chore: fix formatting

* chore: fix CI types

* chore: initial work to drastically improve docs

* docs: improve docs for validation

* docs: add adapter validation docs
2023-10-18 02:22:05 -07:00

6.4 KiB

id, title
id title
form-validation Form and Field Validation

Form and Field Validation

At the core of TanStack Form's functionalities is the concept of validation. We currently support three mechanisms of validation:

  • Synchronous functional validation
  • Asynchronous functional validation
  • Adapter-based validation

Let's take a look at each and see how they're built.

Synchronous Functional Validation

With Form, you can pass a function to a field and, if it returns a string, said string will be used as the error:

<form.Field
  name="age"
  onChange={val => val < 13 ? "You must be 13 to make an account" : undefined}
  children={(field) => {
    return (
      <>
        <label htmlFor={field.name}>First Name:</label>
        <input
          name={field.name}
          value={field.state.value}
          onBlur={field.handleBlur}
          type="number"
          onChange={(e) => field.handleChange(e.target.valueAsNumber)}
        />
        {field.state.meta.touchedErrors ? (
          <em>{field.state.meta.touchedErrors}</em>
        ) : null}
      </>
    );
  }}
/>

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}
  children={(field) => {
    return (
      <>
        {/* ... */}
        {field.state.meta.errors ? (
          <em>{field.state.meta.errors}</em>
        ) : null}
      </>
    );
  }}
/>

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}
  children={(field) => {
    return (
      <>
        {/* ... */}
        {field.state.meta.errorMap['onChange'] ? (
          <em>{field.state.meta.errorMap['onChange']}</em>
        ) : null}
      </>
    );
  }}
/>

Using Alternative Validation Steps

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:

<form.Field
  name="age"
  onBlur={val => val < 13 ? "You must be 13 to make an account" : undefined}
  children={(field) => {
    return (
      <>
        {/* ... */}
      </>
    );
  }}
/>

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="firstName"
  onChangeAsync={async (value) => {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return (
      value.includes("error") && 'No "error" allowed in first name'
    );
  }}
  children={(field) => {
    return (
      <>
        <label htmlFor={field.name}>First Name:</label>
        <input
          name={field.name}
          value={field.state.value}
          onBlur={field.handleBlur}
          onChange={(e) => field.handleChange(e.target.value)}
        />
        <FieldInfo field={field} />
      </>
    );
  }}
/>

This can be combined with the respective synchronous properties as well:

<form.Field
  name="firstName"
  onChange={(value) =>
    !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) => {
    return (
      <>
		{/* ... */}
      </>
    );
  }}
/>

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="firstName"
  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="firstName"
  asyncDebounceMs={500}
  onChangeAsyncDebounceMs={1500}
  onChangeAsync={async (value) => {
    // ...
  }}
  onBlurAsync={async (value) => {
    // ...
  }}
  children={(field) => {
    return (
      <>
		{/* ... */}
      </>
    );
  }}
/>

This will run onChangeAsync every 1500ms while onBlurAsync will run every 500ms.

Adapter-Based Validation

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 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

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="firstName"
  validator={zodValidator}
  onChange={z
    .string()
    .min(3, "First name must be at least 3 characters")}
  children={(field) => {
    return (
      <>
         {/* ... */}
      </>
    );
  }}
/>

These adapters also support async operations using the proper property names:

<form.Field
  name="firstName"
  onChange={z
    .string()
    .min(3, "First name must be at least 3 characters")}
  onChangeAsyncDebounceMs={500}
  onChangeAsync={z.string().refine(
    async (value) => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return !value.includes("error");
    },
    {
      message: "No 'error' allowed in first name",
    },
  )}
  children={(field) => {
    return (
      <>
         {/* ... */}
      </>
    );
  }}
/>