Getting started
A first form with validation, built in the order you would build it yourself.
Every formdef app has the same setup: the contexts, a registry that names its widgets, and one config per form. This page builds them one at a time, ending with a form that validates and submits.
The contexts
TanStack Form needs two React contexts. Create them once per app, in a file of their own:
'use client'
import { createFormHookContexts } from '@tanstack/react-form'
export const { , , useFieldContext, useFormContext } =
createFormHookContexts()'use client' is required: the contexts are React contexts, which a server component cannot
create. They live apart from the registry on purpose. A form component you register later reads
the form through useFormContext(); taking it from the registry, which imports that component,
would make the two files import each other.
A widget
A widget is a React component that receives the field as a prop. FieldOf<string> says which
value it serves:
import { type , } from '@formdef/core'
type = <string> & { : string }
export const = ({ , }: ) => {
const { , , , } = ()
return (
<div>
<label htmlFor={.}>{}</label>
<input
{...}
value={.state.}
onChange={() => .(..)}
/>
{ && . > 0 && <p id={}>{.(' ')}</p>}
</div>
)
}fieldis TanStack's own field:state.value,handleChange,state.meta, unchanged.labelis the widget's own prop. A config passes it throughprops, like any other.fieldPartsdoes the wiring: an id unique to the form, the name, the blur handler and the aria attributes that tie the input to its error.isErrorDueturns true once the field was edited and left, or the form was submitted.
The registry
The registry binds the contexts and the widgets under names. Everything you build forms with comes from it:
'use client'
import { } from '@formdef/core'
import { , } from './context'
import { } from './text-field'
export const { createForm, Fields, } = ({
,
,
: { : },
})The keys of widgets are the names a config uses; they are yours to choose.
A form
The config names a schema, the default values and a widget per field. createForm turns it into a
component:
'use client'
import { } from 'zod'
import { createForm } from './form/registry'
const = .({
: .('Enter an email address'),
: .().(8, 'Use at least 8 characters'),
})
export const = createForm({
,
: { : '', : '' },
fields: {
email: { : 'text', : { : 'Email' } },
password: { : 'text', : { : 'Password' } },
},
onSubmit: ({ }) => {
.()
},
})SignInForm is a client component; a server component renders <SignInForm /> like any other.
onSubmit runs only once the schema accepts the values.
A submit button
The form has no button yet: buttons are components, and components come from you. Register one
under formComponents. It reads the form through useFormContext() from the contexts file:
import { useFormContext } from './context'
export const = ({ }: { : string }) => {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.}>
{() => (
<button type="submit" aria-disabled={}>
{}
</button>
)}
</form.Subscribe>
)
}Add formComponents: { SubmitButton } to the registry, then place the button with a layout. A
layout decides the order and what appears at all:
export const = createForm({
,
: { : '', : '' },
fields: {
email: { : 'text', : { : 'Email' } },
password: { : 'text', : { : 'Password' } },
},
: ['email', 'password', { : 'SubmitButton', : { : 'Sign in' } }],
})Values from a hook
createForm runs once, at module level, so the config cannot close over a hook's result: a
mutation, a router, a toast. Those go into the instance through options:
export const = () => {
const = ()
return (
<
={{
onSubmit: async ({ }) => {
await .()
},
}}
/>
)
}options takes TanStack's form options and wins over the config key by key. validators and
listeners merge per key, so the instance adds to them without dropping the config's.
What the compiler checks
Each of these mistakes stops the build, with the message at the line that makes it.
A default value of the wrong type:
export const = createForm({
,
: { email: 0, password: '' }, fields: { email: { : 'text', : { : 'Email' } } },
})A prop the widget does not take:
export const = createForm({
,
: { : '', : '' },
fields: { email: { : 'text', : { : 'Email', rows: 3 } } },})A field the schema does not have:
export const = createForm({
,
: { : '', : '' },
fields: { pasword: { : 'text', : { : 'Password' } } },})Widgets and the type check lists everything the check covers, and where it ends.