T
TanStack11mo ago
useful-bronze

Form-level onBlur validation shows error messages during typing for required length fields

I've noticed an unexpected behavior with form-level onBlur validation in TanStack Form. When using a Zod schema for validation with onBlur, typing in a field that has a minimum length requirement shows validation errors immediately during typing, before the user has finished entering their input or left the field. Example scenario: 1. Form has multiple fields with Zod validation (e.g., first_name requires min 2 chars) 2. Form setup:
useForm({
validators: {
onBlur: zodSchema
}
})
useForm({
validators: {
onBlur: zodSchema
}
})
3. User starts typing in first_name field 4. After typing just 1 character, the "minimum 2 characters" error appears immediately 5. We're using isTouched to prevent premature error display in other fields:
{state.meta.errors.length > 0 && state.meta.isTouched && (
<ErrorMessage>{state.meta.errors.join(", ")}</ErrorMessage>
)}
{state.meta.errors.length > 0 && state.meta.isTouched && (
<ErrorMessage>{state.meta.errors.join(", ")}</ErrorMessage>
)}
Current behavior: - Error messages appear while typing, before the field is blurred - This creates a poor UX where users see validation errors before they've finished their input Expected behavior: - Validation errors should only show after the user has completed their input and left the field (true onBlur behavior) - Or at least provide a way to distinguish between "currently typing" and "finished typing" states for validation display Is this the intended behavior? If so, what's the recommended pattern for handling minimum length validations without showing errors prematurely?
18 Replies
foreign-sapphire
foreign-sapphire11mo ago
It's a bug, there's an ongoing PR to fix it here: https://github.com/TanStack/form/pull/940
useful-bronze
useful-bronzeOP11mo ago
Good to know thanks @ksgn , I was unsure if I was doing something wrong
exotic-emerald
exotic-emerald5mo ago
looks like this was merged but I'm still experiencing the same issue. Here's a video showing the behavior with the relevant code.
foreign-sapphire
foreign-sapphire5mo ago
What version are you on? What do your fields look like (code)?
exotic-emerald
exotic-emerald5mo ago
@tanstack/react-form: 1.6.3
<form.AppField
name='email'
children={field => (
<field.TextField
label='Email'
type='email'
autoComplete='email'
placeholder='Enter your email'
/>
)}
/>

<form.AppField
name='username'
children={field => (
<field.TextField
label='Username'
type='text'
placeholder='Enter your username'
/>
)}
/>

<form.AppField
name='tag'
children={field => (
<field.TextField
label='Tag'
type='text'
placeholder='Enter your tag'
/>
)}
/>

<form.AppField
name='password'
children={field => (
<field.TextField
label='Password'
type='password'
autoComplete='new-password'
placeholder='•••••••••••'
/>
)}
/>
<form.AppField
name='email'
children={field => (
<field.TextField
label='Email'
type='email'
autoComplete='email'
placeholder='Enter your email'
/>
)}
/>

<form.AppField
name='username'
children={field => (
<field.TextField
label='Username'
type='text'
placeholder='Enter your username'
/>
)}
/>

<form.AppField
name='tag'
children={field => (
<field.TextField
label='Tag'
type='text'
placeholder='Enter your tag'
/>
)}
/>

<form.AppField
name='password'
children={field => (
<field.TextField
label='Password'
type='password'
autoComplete='new-password'
placeholder='•••••••••••'
/>
)}
/>
text-field.tsx
export function TextField({ label, ...props }: TextFieldProps) {
const field = useFieldContext<string>()
const meta = field.state.meta

return (
<div className='grid'>
<Label htmlFor={field.name} className='mb-1'>
{label}
</Label>
<Input
id={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
aria-invalid={meta.isTouched && meta.errors.length > 0}
{...props}
/>
<div className='mt-1'>
<FieldErrors meta={meta} />
</div>
</div>
)
}
export function TextField({ label, ...props }: TextFieldProps) {
const field = useFieldContext<string>()
const meta = field.state.meta

return (
<div className='grid'>
<Label htmlFor={field.name} className='mb-1'>
{label}
</Label>
<Input
id={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={e => field.handleChange(e.target.value)}
aria-invalid={meta.isTouched && meta.errors.length > 0}
{...props}
/>
<div className='mt-1'>
<FieldErrors meta={meta} />
</div>
</div>
)
}
flat-fuchsia
flat-fuchsia5mo ago
when accessing meta, try using useStore for reactivity
foreign-sapphire
foreign-sapphire5mo ago
In your TextField function the meta is stale - you need to make it reactive. https://tanstack.com/form/latest/docs/framework/react/guides/reactivity
Reactivity | TanStack Form React Docs
Tanstack Form doesn't cause re-renders when interacting with the form. So you might find yourself trying to use a form or field state value without success. If you would like to access reactive values...
exotic-emerald
exotic-emerald5mo ago
ah... gotcha thanks for the info both of you! even after trying const meta = useStore(field.store, state => state.meta) for accessing the meta and directly accessing it from field it still seems to behave the same
flat-fuchsia
flat-fuchsia5mo ago
try making one per field you're accessing so isTouched and errors
exotic-emerald
exotic-emerald5mo ago
gotcha let me try that really quick still the same behavior
const isTouched = useStore(field.store, state => state.meta.isTouched)

const errors = useStore(field.store, state => state.meta.errors)
const isTouched = useStore(field.store, state => state.meta.isTouched)

const errors = useStore(field.store, state => state.meta.errors)
flat-fuchsia
flat-fuchsia5mo ago
:Hmm: can you set up the code in stackblitz? see if it is reproducible
exotic-emerald
exotic-emerald5mo ago
sure give me a bit sorry that took a while but here you go https://stackblitz.com/edit/tanstack-form-ge4ijvnj?file=src%2Findex.tsx
flat-fuchsia
flat-fuchsia5mo ago
I‘ll take a look after lunch :PepeThumbs: seems to freeze whenever I open it. What's up with stackblitz at the moment 🤨
flat-fuchsia
flat-fuchsia5mo ago
No description
exotic-emerald
exotic-emerald5mo ago
weird… I’ll double check to see if it’s something I did wrong after I wake up
flat-fuchsia
flat-fuchsia5mo ago
firefox moment :Bueno: works on chrome Three notes: * You pass the meta to another component. Reuse useStore where you can, as it ensures reactivity and prevents unnecessary rerenders (in this case in FieldErrors) * Any component as child (or child of child etc.) of AppField has access to fieldContext, so FieldErrors can get the field variable itself. * The behaviour seems to be as expected. Once a field is focused and changed, it becomes touched. You have a validator on blur, but hide the error if it's not touched. The form validator validates all fields on blur. That means you will receive errors in 'unrelated' fields too. Solution: You might be looking for isBlurred && isTouched instead.
exotic-emerald
exotic-emerald5mo ago
Gotcha I was able to update my implementation using your recommendations and got everything working exactly how I wanted. Thanks for your time and help!
flat-fuchsia
flat-fuchsia5mo ago
glad to hear! If you find more issues, you know where to find me

Did you find this page helpful?