· tanstack-form · 5 min read

TanStack Form: Advanced Validation

Showcasing some advanced TanStack Form validation features.

Showcasing some advanced TanStack Form validation features.

Validation is one of the core features of forms, probably the most important one.

In the last article we set up together a simple form with TanStack Form and basic validation. Today we’ll push it a bit further and see how to handle more complex validation scenarios. In this order:

  1. Form-level validation
  2. Backend validation
  3. Local + Async validation
  4. User feedback during validation
  5. Listen and validate on linked fields
  6. Validating libraries (zod, yup, valibot)
TanStack Logo
Learn More About TanStack

Discover all 21 tutorials I've created on Router and other TanStack libraries for web developers

Browse Tutorials

Follow along

As usual I also recorded a demo where I set up everything which I recommend you to watch and follow along, you can find it here:

Interested in the source code? Here on GitHub: https://github.com/Balastrong/tanstack-form-demo/tree/02-advanced-validation

Here below you can find the most important steps and code snippets, let’s begin!

Form-level validation

In the first video we added validators to the form.Field components to have field-level validation. Guess what, we can also add validators to the form component to have form-level validation!

const form = useForm({
  defaultValues: {
    username: '',
    password: '',
  },
  validators: {
    onSubmit: ({ value }) => {
      if (!value.username || !value.password) {
        return 'Please fill in all fields';
      }
    },
  },
  onSubmit: ({ value }) => {
    console.log(value);
  },
});

Spoiler: soon it will be possible to validate fields in the form validator too! See: https://github.com/TanStack/form/pull/656

Backend validation

Sometimes you don’t have enough data on the frontend to do a very specific validation. An example? Checking if a username is already taken. In this case you can send the data to the backend and get a response back.

To reproduce this I created a method returning a promise that resolves after 1 second, mocking a backend. If you’re following along with the tutorial and you don’t have a backend to test with, here’s what I used in the video:

export function validateUsername(username: string) {
  return new Promise<string | undefined>((resolve) => {
    console.log('Validating username: ' + username);
    setTimeout(() => {
      resolve(['foo', 'bar', 'baz'].includes(username) ? 'Username already taken' : undefined);
    }, 1000);
  });
}

We can now use it inside our username validator, having this as a result:

 <form.Field
    name="username"
    validatorAdapter={zodValidator}
    validators={{
        onChangeAsyncDebounceMs: 100,
        onChangeAsync: ({ value }) => validateUsername(value),
    }}
    children={(field) => (
        ...
    )}
/>

With that the async validation runs after 100ms of inactivity on the input field and performs our (mock) backend validation.

Local + Async validation

To be clear, you can have both local and async validation at the same time. There’s also one optimization you might want to know: the async validation will run after the local validation passes.

This perfectly works:

<form.Field
  name="username"
  validatorAdapter={zodValidator}
  validators={{
    onChangeAsyncDebounceMs: 100,
    onChangeAsync: ({ value }) => validateUsername(value),
    onChange: ({ value }) => value.length < 3 && 'Username must be at least 3 characters long',
  }}
  children={(field) => ...}
/>

User feedback during validation

If your validation takes a little bit of time (even half a second) you might want to inform your user that something is going on. Tanstack Form exposes a properly called isValidating which comes to the rescue!

<form.Field
  name="username"
  validatorAdapter={zodValidator}
  validators={{
    onChangeAsyncDebounceMs: 100,
    onChangeAsync: ({ value }) => validateUsername(value),
    onChange: ({ value }) => value.length < 3 && 'Username must be at least 3 characters long',
  }}
  children={(field) => (
    <div className="relative">
      <Input id="username" type="text" value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
      {field.getMeta().isValidating && (
        <div className="absolute right-2 top-1/2 transform -translate-y-1/2">
          <LoaderCircle className="animate-spin" />
        </div>
      )}
    </div>
  )}
/>

Listen and validate on linked fields

Here’s another common usecase: you want to validate a field based on the value of another field AND make sure to run the validation when the OTHER field changes.

There you have: onChangeListensTo:

<form.Field
  name="confirmPassword"
  validators={{
    onChangeListenTo: ['password'],
    onChange: ({ value, fieldApi }) =>
      fieldApi.getMeta().isDirty && value !== fieldApi.form.getFieldValue('password') && 'Passwords do not match',
  }}
  children={(field) => (
    <>
      <Label htmlFor="password">Confirm Password</Label>
      <Input
        id="confirmPassword"
        type="password"
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {field.state.meta.errors && <p className="text-destructive text-sm mt-1">{field.state.meta.errors}</p>}
    </>
  )}
/>

Adding onChangeListenTo: ['password'] to the confirmPassword field will make sure that the validation runs when the password field changes.

Validating libraries (zod, yup, valibot)

Do you have schemas of your data? You can use them to validate your form fields! TanStack Form supports zod, yup and valibot out of the box.

You can pass the schema definition and make it work by also passing an adapter to the form.Field component:

Let’s make an example with zod and its adapter, zodValidator:

npm i zod @tanstack/zod-form-adapter

After installing both zod and @tanstack/zod-form-adapter you can use it like this:

 <form.Field
    name="username"
    validatorAdapter={zodValidator}
    validators={{
        onChange: z.string().min(3)
    }}
    children={(field) => (
        ...
    )}
/>

Or if you have the schema defined somewhere else:

const UsernameSchema = z
  .string()
  .min(3, "Username must be at least 3 characters");

...
 <form.Field
    name="username"
    validatorAdapter={zodValidator}
    validators={{
        onChange: UsernameSchema
    }}
    children={(field) => (
        ...
    )}
/>

Conclusion

And this was a glimpse into some of the validation options that Tanstack Form is currently supporting.

The next video of this series will probably be about arrays and dynamic fields, so make sure to subscribe to the channel to not miss it!

I hope you enjoyed this article, if you have any questions or feedback feel free to reach out in the comments below. See you in the next one! 👋

About the author
Leonardo

Hello! My name is Leonardo and as you might have noticed, I like to talk about Web Development and Open Source!

I use GitHub every day and my favourite editor is Visual Studio Code... this might influence a little bit my conent! :D

If you like what I do, you should have a look at my YouTube Channel!

tanstack-form (3 Parts Series)
You might also like
Back to Blog