Linked and Dependent Fields
Most forms have at least one field that depends on another. A shipping address that mirrors billing. A phone number that only matters when the user picks "call me." An extra field that unlocks when a checkbox is checked. These are linked fields — their visibility, value, or validation is driven by what's happening elsewhere in the form.
What Makes Fields Linked
A field is linked when one of these things is true:
- It's only visible when another field has a certain value
- Its validation rules change based on another field
- Its value should be copied or derived from another field
The tool TanStack Form gives you for all three is form.Subscribe. It lets you read any slice of form state and react to it in your render.
Conditional Rendering
The most common pattern is showing or hiding a field based on a selection. Wrap the dependent fields in form.Subscribe and use the selector to pull out the controlling value:
<form.Subscribe
selector={(state) => state.values.contactMethod}
children={(contactMethod) => (
<>
{contactMethod === 'email' && (
<form.Field
name="email"
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
)}
{contactMethod === 'phone' && (
<form.Field
name="phone"
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
)}
</>
)}
/>
Only the relevant input renders. The others don't exist in the DOM, so their values don't interfere with submission.
Conditional Validation
Visibility alone isn't always enough. Sometimes you need validation rules that depend on context. Use form.getFieldValue() inside a validator to read another field's current value at the moment validation runs:
<form.Field
name="phone"
validators={{
onBlur: ({ value }) => {
const method = form.getFieldValue('contactMethod')
if (method === 'phone' && !value) return 'Phone is required'
return undefined
},
}}
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
/>
The phone field only fails validation when the user actually chose "phone" as their contact method. Every other time, the validator returns undefined and stays silent.
The "Same As" Pattern
A checkbox that copies values from one group of fields to another is a classic UI pattern — most commonly "shipping address same as billing." When the checkbox is checked, you programmatically set the shipping fields to match the billing values. When unchecked, you clear them so the user can enter different ones.
<form.Field
name="sameAsBilling"
children={(field) => (
<label>
<input
type="checkbox"
checked={field.state.value}
onChange={(e) => {
field.handleChange(e.target.checked)
if (e.target.checked) {
form.setFieldValue('shippingCity', form.getFieldValue('billingCity'))
form.setFieldValue('shippingZip', form.getFieldValue('billingZip'))
} else {
form.setFieldValue('shippingCity', '')
form.setFieldValue('shippingZip', '')
}
}}
/>
Same as billing address
</label>
)}
/>
The checkbox field drives the state. form.getFieldValue reads the source, form.setFieldValue writes to the target. No separate state management needed.
Complete Example
Here's a contact form where the relevant input appears based on the selected contact method:
function ContactForm() {
const form = useForm({
defaultValues: {
contactMethod: 'email',
email: '',
phone: '',
mailingAddress: '',
},
onSubmit: async ({ value }) => console.log(value),
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="contactMethod"
children={(field) => (
<select
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
>
<option value="email">Email</option>
<option value="phone">Phone</option>
<option value="mail">Mail</option>
</select>
)}
/>
<form.Subscribe
selector={(state) => state.values.contactMethod}
children={(contactMethod) => (
<>
{contactMethod === 'email' && (
<form.Field
name="email"
validators={{
onBlur: ({ value }) => (!value ? 'Email is required' : undefined),
}}
children={(field) => (
<div>
<input
placeholder="Email address"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors.length > 0 && (
<p style={{ color: 'red' }}>{field.state.meta.errors.join(', ')}</p>
)}
</div>
)}
/>
)}
{contactMethod === 'phone' && (
<form.Field
name="phone"
validators={{
onBlur: ({ value }) => (!value ? 'Phone is required' : undefined),
}}
children={(field) => (
<div>
<input
placeholder="Phone number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors.length > 0 && (
<p style={{ color: 'red' }}>{field.state.meta.errors.join(', ')}</p>
)}
</div>
)}
/>
)}
{contactMethod === 'mail' && (
<form.Field
name="mailingAddress"
validators={{
onBlur: ({ value }) => (!value ? 'Mailing address is required' : undefined),
}}
children={(field) => (
<div>
<input
placeholder="Mailing address"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors.length > 0 && (
<p style={{ color: 'red' }}>{field.state.meta.errors.join(', ')}</p>
)}
</div>
)}
/>
)}
</>
)}
/>
<button type="submit">Submit</button>
</form>
)
}
form.Subscribe watches the contactMethod value and re-renders only the dependent section when it changes. Each field carries its own validation but only appears — and only validates — when it's actually relevant. This keeps the form focused and the user experience clean.