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 tovaluemakes 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.