---
title: TanStack Form + Zod Validation in React (2026)
description: 'Build type-safe React forms with TanStack Form v1 and Zod: form + field validation, async checks, array fields, and submit state. No adapter, all runnable.'
url: https://nerdleveltech.com/tanstack-form-react-zod-validation-tutorial
language: en
date: '2026-06-22'
category: frontend
tags:
  - tanstack form
  - zod
  - react
  - forms
  - validation
  - typescript
  - frontend
markdown_url: https://nerdleveltech.com/tanstack-form-react-zod-validation-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/tanstack-form-react-zod-validation-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# TanStack Form + Zod Validation in React (2026)

To validate a TanStack Form with Zod, pass the Zod schema straight into a validator — `validators: { onChange: schema }` — on the `useForm` hook. TanStack Form v1 supports Standard Schema natively, so you no longer install or import a Zod adapter. The schema validates and infers your form's types at once.[^validation]

## TL;DR

TanStack Form v1.33.0 speaks [Standard Schema](https://standardschema.dev/), so Zod (and Valibot, ArkType, or Effect/Schema) plug in directly — the old `@tanstack/zod-form-adapter` and `zodValidator` are no longer needed.[^validation] This hands-on guide builds a real React form from an empty component: a `useForm` instance validated by a Zod 4 schema, per-field error rendering, debounced async validation, dynamic array fields, and a submit button wired to `canSubmit`. Every snippet was type-checked with `tsc --noEmit` against `@tanstack/react-form@1.33.0`, `zod@4.4.3`, and React 19 on 22 June 2026.

## What you'll learn

- How to validate a TanStack Form with Zod (no adapter, just the schema)
- How to set up `useForm` and a `form.Field` in React from scratch
- How to show validation errors correctly (v1 errors are objects, not strings)
- How to add debounced async validation at the field level
- How to handle dynamic array fields with `mode="array"`
- When to reach for TanStack Form instead of React Hook Form

## How do I validate a TanStack Form with Zod?

Pass the Zod schema directly to a `validators` key on `useForm`. There is no adapter step in v1 — TanStack Form treats any Standard Schema library, Zod included, as a first-class validator and uses it to both validate values and type the form.[^validation]

Install the two packages (TanStack Form pulls in its own core; you only add Zod):

```bash
npm install @tanstack/react-form@1.33.0 zod@4.4.3
```

Now define a schema and feed it to the form. Note the Zod 4 idiom `z.email()` — the top-level helper that replaces the older `z.string().email()` chain:

```tsx
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'

const schema = z.object({
  email: z.email('Enter a valid email'),
  password: z.string().min(8, 'At least 8 characters'),
})

export function SignupForm() {
  const form = useForm({
    defaultValues: { email: '', password: '' },
    validators: { onChange: schema },
    onSubmit: async ({ value }) => {
      // value is fully typed: { email: string; password: string }
      console.log('submit', value)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      {/* fields go here */}
    </form>
  )
}
```

`validators.onChange` runs the schema on every change; swap it for `onBlur` or `onSubmit` to validate later. Because the schema also describes the shape, `value` inside `onSubmit` is typed as `{ email: string; password: string }` with no extra generics.[^quickstart]

## How do I set up a form.Field in React?

`form.Field` is a render-prop component: you give it a `name` and a function that receives the field's state and handlers. This keeps each input subscribed only to its own slice of form state, so typing in one field doesn't re-render the others.[^quickstart]

Drop a field inside the `<form>` from the previous section:

```tsx
<form.Field name="email">
  {(field) => (
    <div>
      <label htmlFor={field.name}>Email</label>
      <input
        id={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.value)}
      />
    </div>
  )}
</form.Field>
```

The three pieces you wire on every input are `field.state.value` (the current value), `field.handleChange` (update it), and `field.handleBlur` (mark it touched). The `name` is type-checked against your `defaultValues`, so a typo like `name="emial"` is a compile error, not a silent runtime bug.

## How do I show validation errors in TanStack Form?

Read `field.state.meta.errors` — but in v1 each error from a Standard Schema validator is an **issue object**, so render `error.message`, not the raw value. This is a common mistake when migrating from older adapter-based examples, which exposed errors as plain strings.[^validation]

```tsx
<form.Field name="email">
  {(field) => (
    <div>
      <input
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {!field.state.meta.isValid && (
        <em role="alert">
          {field.state.meta.errors.map((err) => err?.message).join(', ')}
        </em>
      )}
    </div>
  )}
</form.Field>
```

Use `field.state.meta.isValid` to decide whether to show the message, and map over `errors` with `err?.message` because the array holds objects. (If you instead write a plain function validator that returns a string, that string is the error directly — see the next section. The two validator styles produce two different error shapes, which trips people up.)

## How do I disable the submit button until the form is valid?

Wrap the button in `form.Subscribe` and select `canSubmit`. `form.Subscribe` re-renders only when the slice you select changes, so the button reacts to validity and submission state without re-rendering the whole form.[^quickstart]

```tsx
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
  {([canSubmit, isSubmitting]) => (
    <button type="submit" disabled={!canSubmit}>
      {isSubmitting ? 'Submitting…' : 'Create account'}
    </button>
  )}
</form.Subscribe>
```

`canSubmit` reflects whether the form can be submitted — it turns `false` once a validator is failing or an async check is still running — and `isSubmitting` is `true` while your `onSubmit` promise resolves, enough to disable the button and show a spinner without tracking that state yourself.

## How do I add async validation in TanStack Form?

Add an `onChangeAsync` validator (plus `onChangeAsyncDebounceMs`) at the field level for checks that hit the network, like "is this username taken?" Async validators can be debounced — set `onChangeAsyncDebounceMs` so you don't fire a request on every keystroke — and they run only after the synchronous validators pass.[^validation]

```tsx
<form.Field
  name="username"
  validators={{
    onChange: ({ value }) =>
      value.length < 3 ? 'Too short' : undefined,
    onChangeAsyncDebounceMs: 500,
    onChangeAsync: async ({ value }) => {
      const res = await fetch(`/api/username-available?u=${value}`)
      const { available } = await res.json()
      return available ? undefined : 'Username is taken'
    },
  }}
>
  {(field) => (
    <div>
      <input
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {field.state.meta.isValidating && <span>Checking…</span>}
      {!field.state.meta.isValid && (
        <em>{field.state.meta.errors.join(', ')}</em>
      )}
    </div>
  )}
</form.Field>
```

Here the validators are plain functions that **return a string** (or `undefined` when valid), so `errors` holds strings and you render them directly — no `.message`. `field.state.meta.isValidating` is `true` while the debounced async check is in flight, which is what you bind a "Checking…" indicator to.

## How do I handle array fields in TanStack Form?

Give the parent `form.Field` `mode="array"`, then render one child field per item using an indexed name like `guests[${i}].name`. Add and remove rows with `field.pushValue()` and `field.removeValue()` — the list re-renders itself.[^arrays]

```tsx
<form.Field name="guests" mode="array">
  {(field) => (
    <div>
      {field.state.value.map((_, i) => (
        <form.Field key={i} name={`guests[${i}].name`}>
          {(sub) => (
            <input
              value={sub.state.value}
              onChange={(e) => sub.handleChange(e.target.value)}
            />
          )}
        </form.Field>
      ))}
      <button type="button" onClick={() => field.pushValue({ name: '' })}>
        Add guest
      </button>
      <button
        type="button"
        onClick={() => field.removeValue(field.state.value.length - 1)}
      >
        Remove last
      </button>
    </div>
  )}
</form.Field>
```

A Zod schema like `z.array(z.object({ name: z.string().min(1) })).min(1)` validates the whole array at the form level, so "add at least one guest" and "every guest needs a name" both come from the same schema you already wrote.

## When should I use TanStack Form instead of React Hook Form?

Choose TanStack Form when you want type-safe field paths, built-in async validation, and one mental model shared with TanStack Query or Router; stay on React Hook Form when you have simple-to-moderate forms and value its maturity and large ecosystem.[^comparison] Neither is universally "better" — they make different tradeoffs.

React Hook Form is ref-based and uncontrolled, which keeps re-renders low and the API small, and it has years of adoption and integrations behind it. TanStack Form uses a reactive store with fine-grained subscriptions, fully typed field names, a framework-agnostic core (React, Vue, Solid, Svelte, Angular), and Standard Schema validation in the box.[^comparison] If your forms are working and you aren't hitting type-safety or async-validation pain, there's no urgency to switch.

## FAQ

**Do you still need a Zod adapter for TanStack Form?**
No. TanStack Form v1 supports Standard Schema natively, so you pass the Zod schema directly to a validator. The separate `@tanstack/zod-form-adapter` package and its `zodValidator` are not used in v1 — examples that import them are out of date.[^validation]

**How do I validate a TanStack Form with Zod?**
Define a Zod schema and assign it to `validators.onChange` (or `onBlur`/`onSubmit`) on `useForm`. The schema validates values and infers the form's types in one step.[^validation]

**Why are my TanStack Form errors `[object Object]`?**
Because Standard Schema validators return issue objects, not strings. Render `error.message` for each entry in `field.state.meta.errors`. Plain function validators, by contrast, return the error string directly.[^validation]

**How do I add async validation?**
Use `onChangeAsync` on a field's `validators`, and set `onChangeAsyncDebounceMs` to debounce it. The async check runs only after the sync validators pass; bind `field.state.meta.isValidating` to a loading indicator.[^validation]

**How do I add or remove items in an array field?**
Set `mode="array"` on the parent field and call `field.pushValue(item)` and `field.removeValue(index)`. Render children with indexed names such as `items[${i}].value`.[^arrays]

**Which TanStack Form version is current?**
`@tanstack/react-form@1.33.0` is the latest release as of 22 June 2026, with a React peer range of `^17 || ^18 || ^19`.

## Bottom line

TanStack Form v1 makes Zod a one-line validator: pass the schema, render `err.message`, and let `canSubmit` drive the button — no adapter, no glue code. Build the synchronous form first, then layer in debounced async checks and array fields as your form grows. If you work across the TanStack ecosystem, see our [TanStack Query prefetch guide for the Next.js App Router](https://nerdleveltech.com/tanstack-query-nextjs-app-router-prefetch-tutorial) and our walkthrough of [type-safe search params with TanStack Router and Zod](https://nerdleveltech.com/tanstack-router-type-safe-search-params-zod-tutorial). For another take on form submission and pending UI, compare this with [React 19 server actions and optimistic UI](https://nerdleveltech.com/nextjs-16-server-actions-react-19-optimistic-ui-tutorial).

[^validation]: TanStack Form — Form and Field Validation (React). https://tanstack.com/form/latest/docs/framework/react/guides/validation
[^quickstart]: TanStack Form — Quick Start (React). https://tanstack.com/form/latest/docs/framework/react/quick-start
[^arrays]: TanStack Form — Arrays (React). https://tanstack.com/form/v1/docs/framework/react/guides/arrays
[^comparison]: TanStack Form — Comparison. https://tanstack.com/form/latest/docs/comparison
