Lesson 3

Your First Form

Let's build a real form. By the end of this lesson you'll have a working form with two fields and a submit button — all wired up through TanStack Form.

Setting Up useForm

Everything starts with useForm. Import it and call it at the top of your component:

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

function MyForm() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    onSubmit: async ({ value }) => {
      console.log(value)
    },
  })
}

Two things to pay attention to here.

defaultValues defines the shape of your form data. Every field you add later needs to match a key here. TanStack Form uses this to set up its internal state and to infer types — so if you're using TypeScript, you get accurate types on your field values automatically.

onSubmit is where you handle a successful submission. It receives { value }, which is the current form data — the same shape as defaultValues, but filled in with whatever the user typed. It's async, so you can await an API call inside it directly.

Wiring Up the Form Element

Next, return a <form> element and hook up the submit handler:

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

The e.preventDefault() call stops the browser's default form submission behavior. Then form.handleSubmit() takes over — it runs validation, and if everything passes, it calls the onSubmit function you defined above.

Adding Your First Field

Now let's add a field. TanStack Form uses a form.Field component with a render children pattern:

<form.Field
  name="firstName"
  children={(field) => (
    <div>
      <label>First Name</label>
      <input
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
        onBlur={field.handleBlur}
      />
    </div>
  )}
/>

The name prop must match one of the keys in defaultValues — in this case "firstName". TypeScript will warn you if you mistype it.

Inside the render function you get a field object. Here's what each piece does:

  • field.state.value — the current value of this field. Passing it to value makes the input controlled.
  • field.handleChange — call this whenever the input changes. It updates the field's value in TanStack Form's state.
  • field.handleBlur — call this when the input loses focus. TanStack Form uses this to track whether a field has been "touched", which matters for when you want to show validation errors.

Adding the Second Field

The lastName field follows the exact same pattern:

<form.Field
  name="lastName"
  children={(field) => (
    <div>
      <label>Last Name</label>
      <input
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
        onBlur={field.handleBlur}
      />
    </div>
  )}
/>

The Complete Form

Here's everything put together:

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

function MyForm() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    onSubmit: async ({ value }) => {
      console.log(value)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      <form.Field
        name="firstName"
        children={(field) => (
          <div>
            <label>First Name</label>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
              onBlur={field.handleBlur}
            />
          </div>
        )}
      />

      <form.Field
        name="lastName"
        children={(field) => (
          <div>
            <label>Last Name</label>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
              onBlur={field.handleBlur}
            />
          </div>
        )}
      />

      <button type="submit">Submit</button>
    </form>
  )
}

export default MyForm

Run this, fill in both fields, and hit Submit. You'll see the form values logged to the console as { firstName: '...', lastName: '...' }. That's TanStack Form doing its job — managing state, tracking interactions, and handing you clean data on submit.

You now have a working form that tracks field values and hands you clean data on submit. Try adding a third field — an email address — and logging it alongside the name fields to get a feel for how form.Field scales.