Lesson 12

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.