---
id: typed-issue-form
track: learn
product: form
locale: en
order: 19
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "Form ^1"
verifiedAgainst: "@tanstack/react-form@1.33.3"
contentModel: 2
prerequisites:
  - "Controlled inputs and form submit events"
  - "A create endpoint with independent server validation"
sourceRefs:
  - "https://tanstack.com/form/latest/docs/framework/react/quick-start"
  - "https://tanstack.com/form/latest/docs/framework/react/guides/validation"
  - "https://tanstack.com/form/latest/docs/framework/react/guides/submission-handling"
---

# Keep fields, validation, and submission on one type

> Build a controlled form with useForm, Field, and Subscribe, making validation timing, error display, and async submission explicit.

**Outcome:** Implement an issue form with inferred field names, intentional error timing, and duplicate-submit protection.

## Default values establish the form input type

useForm infers field paths and the onSubmit value from defaultValues. Each Field reads field.state.value and advances state through field.handleChange and field.handleBlur. Do not mix local React state, DOM defaultValue, and Form-controlled values or you create multiple sources of truth.

## Separate validation timing from error presentation

onBlur avoids showing errors for every keystroke, while onChange suits immediate constraints. Validators return a truthy error on failure and undefined on success. Errors may exist in field meta, but presentation should usually also check isTouched so the initial render does not blame the user.

File: `src/features/issues/IssueForm.tsx`

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

export function IssueForm() {
  const form = useForm({
    defaultValues: { title: '', priority: 'medium' as 'low' | 'medium' | 'high' },
    onSubmit: async ({ value }) => { await createIssue(value) },
  })
  return <form onSubmit={(event) => { event.preventDefault(); form.handleSubmit() }}>
    <form.Field name="title" validators={{
      onBlur: ({ value }) => value.trim() ? undefined : 'Title is required',
    }}>{(field) => <label>Title<input name={field.name} value={field.state.value}
      onBlur={field.handleBlur} onChange={(event) => field.handleChange(event.target.value)} />
      {field.state.meta.isTouched && !field.state.meta.isValid
        ? <span role="alert">{field.state.meta.errors.join(', ')}</span> : null}
    </label>}</form.Field>
    <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
      {([canSubmit, isSubmitting]) => <button type="submit"
        aria-disabled={!canSubmit || isSubmitting} disabled={isSubmitting}>
        {isSubmitting ? 'Saving…' : 'Create issue'}
      </button>}
    </form.Subscribe>
  </form>
}
```

## Submission state is not a server trust boundary

Prevent the native submit and call form.handleSubmit; onSubmit runs only after validation passes. isSubmitting covers the asynchronous handler and can prevent duplicate actions while showing progress. Client types and validators are bypassable, so a Server Function or Server Route must parse and authorize again, returning distinguishable field and system errors.

## Practice and verification

### Completion checkpoint

Field names, defaults, validators, and onSubmit values share one type, with errors shown at an intentional interaction point.

### Exercise

Build an issue form with title and priority: validate title on blur, expose submission progress, and prevent duplicate submits.

### Verification

Test an empty title, valid submit, async failure, and rapid double-click; invalid values never reach onSubmit, only one request runs while pending, and the server validates independently.

### Common pitfalls

- Passing both defaultValue and a controlled value to the input.
- Trusting browser validation and writing the raw value directly on the server.

## Official sources

- https://tanstack.com/form/latest/docs/framework/react/quick-start
- https://tanstack.com/form/latest/docs/framework/react/guides/validation
- https://tanstack.com/form/latest/docs/framework/react/guides/submission-handling
