Lesson 4

Understanding Field State

Every field in TanStack Form carries a field.state object that tells you everything happening with that field at any given moment. Understanding what's in there — and when to reach for each piece — is key to building forms that feel polished.

field.state.value

This is the current value of the field. Whatever the user has typed or selected, that's what you'll find here. You wire it up to your input's value prop to keep the input controlled:

<input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />

Simple enough. Now let's look at the more interesting part: field.state.meta.

field.state.meta

meta is an object that tracks the lifecycle of a field — how the user has interacted with it, whether it's valid, and whether anything async is still running. Here's a breakdown of each property.

isTouched

field.state.meta.isTouched becomes true after the user has focused and then blurred the field. It stays false if the user has never interacted with the field at all.

This is the property you'll reach for most often when deciding whether to show validation errors. You generally don't want to yell at users before they've even had a chance to type anything.

isDirty and isPristine

field.state.meta.isDirty is true when the current value differs from the field's value in defaultValues. If the user clears a pre-filled field, it's dirty. If they type something and then erase it back to the original, it's no longer dirty.

field.state.meta.isPristine is the opposite — true when the value matches defaultValues. These two are always inverses of each other.

These are useful for things like enabling a "Save" button only when something has actually changed.

errors

field.state.meta.errors is an array of error strings produced by your validators. When the field is valid, it's an empty array. When something fails, each failing validator pushes a string into this array.

If you have multiple validators, they can each contribute an error independently — which is why it's an array rather than a single string.

isValidating

field.state.meta.isValidating is true while an async validator is running. This lets you show a "Validating..." indicator so users know something is happening in the background, rather than leaving them to wonder why nothing has happened yet.

Showing Errors Only After Touch

Here's a complete example that wires up an email field with an onBlur validator and only shows the error after the user has interacted with the field:

<form.Field
  name="email"
  validators={{
    onBlur: ({ value }) => !value ? 'Email is required' : undefined,
  }}
  children={(field) => (
    <div>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
        onBlur={field.handleBlur}
      />
      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
        <p style={{ color: 'red' }}>{field.state.meta.errors.join(', ')}</p>
      )}
    </div>
  )}
/>

The validator runs on blur and returns an error string when the value is empty. But the error only renders when isTouched is true — so the first time the page loads, there's no red text staring at the user.

Extracting a FieldInfo Helper

If you have several fields, copy-pasting that error display block gets tedious. A small helper component cleans this up nicely:

import { FieldApi } from '@tanstack/react-form'

function FieldInfo({ field }: { field: FieldApi<any, any, any, any> }) {
  return (
    <>
      {field.state.meta.isTouched && field.state.meta.errors.length > 0 ? (
        <p style={{ color: 'red' }}>{field.state.meta.errors.join(', ')}</p>
      ) : null}
      {field.state.meta.isValidating ? <p>Validating...</p> : null}
    </>
  )
}

Then use it inside any field's render function:

<form.Field
  name="email"
  validators={{
    onBlur: ({ value }) => !value ? 'Email is required' : undefined,
  }}
  children={(field) => (
    <div>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
        onBlur={field.handleBlur}
      />
      <FieldInfo field={field} />
    </div>
  )}
/>

One <FieldInfo /> line replaces the conditional block on every field. It handles both error display and the async validating state, so you're covered as your forms grow more complex.

At this point you understand the metadata TanStack Form tracks for each field and how to surface it cleanly with a reusable component. The same patterns -- isTouched, errors, isValidating -- will show up in every form you build from here on.