Lesson 5

Form State and Lifecycle

The previous lesson covered field-level state — the stuff tracked per input. This lesson goes one level up: the state that lives on the form itself. This is where you find out whether the whole form is valid, whether it's currently submitting, and whether anything has changed since the user landed on the page.

form.state

form.state is the aggregated state object for the entire form. It rolls up everything happening across all fields into a single place. Here are the properties you'll reach for most often:

  • form.state.values — an object containing the current value of every field, keyed by field name. Useful when you need to read multiple values at once outside of a field's render function.
  • form.state.errors — form-level errors. These come from form-level validators you define on useForm, not from individual fields. Field errors live on field.state.meta.errors.
  • form.state.isSubmittingtrue while your onSubmit handler is running. Use this to show a loading state or prevent double-submits.
  • form.state.canSubmittrue when the form has no errors and is not currently submitting. This is the flag to wire up to your submit button's disabled prop.
  • form.state.isDirtytrue if any field's current value differs from defaultValues. Handy for enabling a "Save" button only when the user has actually changed something.
  • form.state.isTouchedtrue if any field has been touched (focused and blurred).
  • form.state.isValidtrue when there are no validation errors across any field or at the form level.

form.Subscribe

When you read from form.state directly in your component's render, that component re-renders whenever the relevant state changes. For complex forms this can mean more re-renders than you need.

form.Subscribe gives you fine-grained control. It lets you subscribe to a specific slice of form state and only re-render that piece of the tree when the selected values actually change. Everything else stays untouched.

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

The selector function receives the full form.state and returns whatever slice you care about. The children function receives that slice as its argument and renders accordingly. The submit button here only re-renders when canSubmit or isSubmitting changes — not every time the user types into a field.

This is the main performance tool TanStack Form gives you. Get in the habit of wrapping anything that reads from form.state in a form.Subscribe.

form.reset()

form.reset() resets all field values back to defaultValues and clears any touched, dirty, and error state. It's the cleanest way to give the user a fresh start without unmounting and remounting the form.

<button type="button" onClick={() => form.reset()}>
  Reset
</button>

Note the type="button" — without it, the button would trigger a form submit inside a <form> element.

Putting It Together

Here's a complete form with a submit button that shows a loading state and a reset button:

function MyForm() {
  const form = useForm({
    defaultValues: { email: '', name: '' },
    onSubmit: async ({ value }) => {
      await submitToServer(value)
    },
  })

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

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

      <button type="button" onClick={() => form.reset()}>
        Reset
      </button>
    </form>
  )
}

The submit button is disabled when the form can't submit and shows "Submitting..." while the async handler is in flight. The reset button wipes everything back to defaults. Between form.Subscribe for targeted re-renders and form.reset() for cleanup, you have everything you need to manage the full lifecycle of a form.