You may need to link two fields together so that one is validated when another’s value changes. A common use case is when you have both a password and a confirm_password field - the confirm_password field should error if its value doesn’t match the password field, regardless of which field triggered the value change.
The Problem
Consider this user flow:
- User updates the
confirm_password field to “password123”
- User updates the
password field to “password456”
In this scenario, the form will still have errors because the confirm_password field’s validation hasn’t re-run to check against the new password value.
The Solution: onChangeListenTo
To solve this, you need to ensure that the confirm_password field’s validation re-runs when the password field is updated. Use the onChangeListenTo prop:
function App() {
const form = useForm({
defaultValues: {
password: '',
confirm_password: '',
},
})
return (
<div>
<form.Field name="password">
{(field) => (
<label>
<div>Password</div>
<input
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)}
</form.Field>
<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
}}
>
{(field) => (
<div>
<label>
<div>Confirm Password</div>
<input
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
{field.state.meta.errors.map((err) => (
<div key={err} style={{ color: 'red' }}>
{err}
</div>
))}
</div>
)}
</form.Field>
</div>
)
}
The onChangeListenTo array accepts multiple field names, so you can link a field to multiple other fields if needed.
How It Works
When you specify onChangeListenTo: ['password'] on the confirm_password field:
- The
onChange validator runs when the confirm_password field itself changes
- The
onChange validator also runs whenever the password field changes
- This ensures the two fields stay in sync from a validation perspective
onBlurListenTo
Similarly, you can use onBlurListenTo to re-run validation when the linked field is blurred:
<form.Field
name="confirm_password"
validators={{
onBlurListenTo: ['password'],
onBlur: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue('password')) {
return 'Passwords do not match'
}
return undefined
},
}}
>
{/* ... */}
</form.Field>
Complete Example
Here’s a complete working example with password matching:
import { useForm } from '@tanstack/react-form'
function PasswordForm() {
const form = useForm({
defaultValues: {
password: '',
confirm_password: '',
},
onSubmit: async ({ value }) => {
console.log('Passwords match!', value)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<form.Field
name="password"
validators={{
onChange: ({ value }) => {
if (value.length < 8) {
return 'Password must be at least 8 characters'
}
return undefined
},
}}
>
{(field) => (
<div>
<label htmlFor={field.name}>Password</label>
<input
id={field.name}
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors && (
<p style={{ color: 'red' }}>{field.state.meta.errors[0]}</p>
)}
</div>
)}
</form.Field>
<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) => {
const password = fieldApi.form.getFieldValue('password')
if (value !== password) {
return 'Passwords do not match'
}
return undefined
},
}}
>
{(field) => (
<div>
<label htmlFor={field.name}>Confirm Password</label>
<input
id={field.name}
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors && (
<p style={{ color: 'red' }}>{field.state.meta.errors[0]}</p>
)}
</div>
)}
</form.Field>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
Other Use Cases
Linked fields are useful for many scenarios:
Date Range Validation
<form.Field
name="endDate"
validators={{
onChangeListenTo: ['startDate'],
onChange: ({ value, fieldApi }) => {
const startDate = fieldApi.form.getFieldValue('startDate')
if (value < startDate) {
return 'End date must be after start date'
}
return undefined
},
}}
>
{/* ... */}
</form.Field>
Conditional Required Fields
<form.Field
name="otherDetails"
validators={{
onChangeListenTo: ['category'],
onChange: ({ value, fieldApi }) => {
const category = fieldApi.form.getFieldValue('category')
if (category === 'other' && !value) {
return 'Please provide details'
}
return undefined
},
}}
>
{/* ... */}
</form.Field>
Dependent Numeric Fields
<form.Field
name="quantity"
validators={{
onChangeListenTo: ['available'],
onChange: ({ value, fieldApi }) => {
const available = fieldApi.form.getFieldValue('available')
if (value > available) {
return `Only ${available} items available`
}
return undefined
},
}}
>
{/* ... */}
</form.Field>
Be careful not to create circular dependencies where Field A listens to Field B and Field B listens to Field A. This can cause infinite validation loops.
Listening to Multiple Fields
You can listen to multiple fields at once:
<form.Field
name="total"
validators={{
onChangeListenTo: ['price', 'quantity', 'discount'],
onChange: ({ value, fieldApi }) => {
const price = fieldApi.form.getFieldValue('price')
const quantity = fieldApi.form.getFieldValue('quantity')
const discount = fieldApi.form.getFieldValue('discount')
const expectedTotal = (price * quantity) - discount
if (Math.abs(value - expectedTotal) > 0.01) {
return `Total should be ${expectedTotal.toFixed(2)}`
}
return undefined
},
}}
>
{/* ... */}
</form.Field>
Use fieldApi.form.getFieldValue(name) to access the value of any field in the form during validation.