Listeners and Side Effects
Validators and listeners both react to field events, but they serve completely different purposes. Validators return error messages — that return value is what TanStack Form uses to set validation state. Listeners return nothing. They exist purely to run side effects when something happens to a field.
If you need to clear a dependent field, trigger a fetch, or auto-format input as the user types, a listener is the right tool.
The listeners Prop
form.Field accepts a listeners object alongside validators. Each key maps to a field event:
<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
// Clear the state field when country changes
form.setFieldValue('state', '')
},
}}
children={(field) => (/* ... */)}
/>
The listener receives the same event context as a validator — including the current value — but whatever you return (or don't return) has no effect on form state other than what you explicitly set yourself.
onChange
Fires every time the field value changes. Common use cases:
- Clearing dependent fields (country changes, so clear the state/province)
- Auto-formatting input
- Fetching related data based on the new value
onBlur
Fires when the field loses focus. Good for things like logging analytics events or triggering an auto-save without wanting to react to every keystroke.
Auto-Formatting Example
Format a phone number as the user types:
<form.Field
name="phone"
listeners={{
onChange: ({ value }) => {
const digits = value.replace(/\D/g, '')
if (digits.length >= 6) {
form.setFieldValue('phone', `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`)
}
},
}}
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
The user types raw digits, the listener intercepts the change and writes back a formatted string. No validator involved — this is purely a side effect.
Cascading Data Fetch Example
When a country field changes, fetch the relevant states or provinces and reset the dependent field:
listeners={{
onChange: async ({ value }) => {
form.setFieldValue('state', '')
const states = await fetchStates(value)
// Update a local state or context with the fetched states
},
}}
Listeners can be async. Just keep in mind there is no built-in debouncing, so for fetch-heavy scenarios you may want to debounce inside the listener yourself.
The Rule of Thumb
Use validators when you need to communicate an error to the user — the return value matters. Use listeners when you need to react to a change with an action — the return value is irrelevant. Keep listeners focused and simple. If a listener is doing too much, that's a sign the logic belongs somewhere else in your application.