Skip to main content

React Form

TanStack React Form is a powerful, type-safe form library for React that provides comprehensive form state management, validation, and field handling with excellent TypeScript support.

Key Features​

  • Type-safe: Full TypeScript support with inferred types
  • Flexible validation: Built-in and custom validation rules
  • Field management: Automatic field registration and state tracking
  • Performance optimized: Minimal re-renders and efficient updates
  • Framework agnostic: Works with any React setup
  • Small bundle size: Tree-shakable and lightweight

Installation​

npm install @tanstack/react-form
note

TanStack React Form requires React 16.8+ and has no other dependencies.

Basic Usage​

Creating a Simple Form​

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

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

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

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

<form.Field
name="email"
children={(field) => (
<input
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
placeholder="Email"
/>
)}
/>

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

Form Configuration​

Default Values​

const form = useForm({
defaultValues: {
user: {
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true,
},
},
},
})

Validation​

TanStack React Form supports multiple validation strategies:

const form = useForm({
defaultValues: {
email: '',
password: '',
},
validators: {
onChange: ({ value }) => {
if (!value.email) return 'Email is required'
if (!value.password) return 'Password is required'
if (value.password.length < 8) return 'Password must be at least 8 characters'
return undefined
},
onSubmit: ({ value }) => {
// Additional validation on submit
return undefined
},
},
})

Field API​

Field State​

Each field provides access to its current state:

<form.Field
name="email"
children={(field) => {
const { state, handleChange, handleBlur } = field

return (
<div>
<input
name={field.name}
value={state.value}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
/>

{state.meta.isTouched && state.meta.errors.length > 0 && (
<div className="error">
{state.meta.errors.map((error) => (
<div key={error}>{error}</div>
))}
</div>
)}
</div>
)
}}
/>

Field Validation​

<form.Field
name="email"
validators={{
onChange: ({ value }) =>
!value
? 'Email is required'
: !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? 'Invalid email format'
: undefined,
onBlur: ({ value }) => {
// Additional blur validation
return undefined
},
}}
children={(field) => (
<input
type="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
/>

Advanced Features​

Array Fields​

function TodoForm() {
const form = useForm({
defaultValues: {
todos: [{ text: '', completed: false }],
},
})

return (
<form onSubmit={form.handleSubmit}>
<form.Field
name="todos"
mode="array"
children={(field) => (
<div>
{field.state.value.map((_, index) => (
<form.Field
key={index}
name={`todos[${index}].text`}
children={(subField) => (
<div>
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
placeholder="Todo item"
/>
<button
type="button"
onClick={() => field.removeValue(index)}
>
Remove
</button>
</div>
)}
/>
))}
<button
type="button"
onClick={() => field.pushValue({ text: '', completed: false })}
>
Add Todo
</button>
</div>
)}
/>
<button type="submit">Submit</button>
</form>
)
}

Conditional Fields​

function ConditionalForm() {
const form = useForm({
defaultValues: {
hasNewsletter: false,
email: '',
},
})

return (
<form onSubmit={form.handleSubmit}>
<form.Field
name="hasNewsletter"
children={(field) => (
<label>
<input
type="checkbox"
checked={field.state.value}
onChange={(e) => field.handleChange(e.target.checked)}
/>
Subscribe to newsletter
</label>
)}
/>

<form.Subscribe
selector={(state) => state.values.hasNewsletter}
children={(hasNewsletter) =>
hasNewsletter ? (
<form.Field
name="email"
children={(field) => (
<input
type="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Email for newsletter"
/>
)}
/>
) : null
}
/>
</form>
)
}

Async Validation​

<form.Field
name="username"
validators={{
onChangeAsync: async ({ value }) => {
if (!value) return 'Username is required'

// Simulate API call
const response = await fetch(`/api/check-username?username=${value}`)
const data = await response.json()

return data.available ? undefined : 'Username already taken'
},
}}
children={(field) => (
<div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isValidating && <span>Checking...</span>}
</div>
)}
/>

Form State Management​

Form State​

function FormWithState() {
const form = useForm({
defaultValues: { name: '' },
})

return (
<div>
<form onSubmit={form.handleSubmit}>
{/* Form fields */}
</form>

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

Reset Form​

const form = useForm({
defaultValues: { name: '', email: '' },
})

// Reset to default values
form.reset()

// Reset to specific values
form.reset({ name: 'John', email: 'john@example.com' })

Integration with UI Libraries​

With React Hook Form Style​

// Custom hook for React Hook Form compatibility
function useField(form: any, name: string) {
return form.Field({
name,
children: (field) => field,
})
}

With Validation Libraries​

TanStack React Form works well with validation libraries like Zod:

import { zodValidator } from '@tanstack/zod-form-validator'
import { z } from 'zod'

const userSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be 18 or older'),
})

const form = useForm({
defaultValues: {
name: '',
email: '',
age: 0,
},
validators: {
onChange: zodValidator(userSchema),
},
})

Best Practices​

Form Structure​

tip

Keep forms modular by breaking complex forms into smaller components. Each component should handle its own validation and state.

Validation Strategy​

note

Use onChange for immediate feedback and onSubmit for final validation. Combine with async validation for server-side checks.

Performance​

warning

Avoid unnecessary re-renders by using form.Subscribe with specific selectors instead of accessing the entire form state.

TypeScript​

interface User {
name: string
email: string
age: number
}

const form = useForm<User>({
defaultValues: {
name: '',
email: '',
age: 0,
},
})

Common Patterns​

Multi-Step Forms​

function MultiStepForm() {
const [step, setStep] = useState(1)
const form = useForm({
defaultValues: {
personal: { name: '', email: '' },
address: { street: '', city: '' },
},
})

const nextStep = () => setStep(step + 1)
const prevStep = () => setStep(step - 1)

return (
<form onSubmit={form.handleSubmit}>
{step === 1 && (
<div>
<h3>Personal Information</h3>
<form.Field
name="personal.name"
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
<button type="button" onClick={nextStep}>Next</button>
</div>
)}

{step === 2 && (
<div>
<h3>Address Information</h3>
<form.Field
name="address.street"
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
<button type="button" onClick={prevStep}>Previous</button>
<button type="submit">Submit</button>
</div>
)}
</form>
)
}

File Upload​

function FileUploadForm() {
const form = useForm({
defaultValues: {
files: [] as File[],
},
})

return (
<form onSubmit={form.handleSubmit}>
<form.Field
name="files"
children={(field) => (
<input
type="file"
multiple
onChange={(e) => {
const files = Array.from(e.target.files || [])
field.handleChange(files)
}}
/>
)}
/>
<button type="submit">Upload</button>
</form>
)
}

API Reference​

useForm Options​

OptionTypeDescription
defaultValuesobjectInitial form values
validatorsobjectForm-level validators
onSubmitfunctionSubmit handler

Field Props​

PropTypeDescription
namestringField name/path
validatorsobjectField validators
childrenfunctionRender function
modestringField mode ('value' or 'array')

Field State​

PropertyTypeDescription
valueanyCurrent field value
metaobjectField metadata (touched, errors, etc.)

Migration from Other Libraries​

From React Hook Form​

// React Hook Form
const { register, handleSubmit, formState } = useForm()

// TanStack React Form
const form = useForm({
defaultValues: { name: '' },
})

From Formik​

// Formik
<Formik initialValues={{ name: '' }}>
<Form>
<Field name="name" />
</Form>
</Formik>

// TanStack React Form
const form = useForm({
defaultValues: { name: '' },
})

Troubleshooting​

Common Issues​

Form not submitting: Ensure e.preventDefault() and e.stopPropagation() are called in the submit handler.

Validation not working: Check that validators return undefined for valid values and error strings for invalid values.

TypeScript errors: Make sure to properly type your defaultValues and use the generic type parameter on useForm.

tip

For more advanced usage and examples, check the official TanStack React Form documentation at tanstack.com/form.