Using zod schema as a form level onChange validator causes form elements to unnecessarily re-render.
Describe the bug
Let's consider the example from the documentation which uses a zod schema as its form-level onChange validator:
https://tanstack.com/form/latest/docs/framework/react/examples/standard-schema
https://6ymfgt-3001.csb.app/
const ZodSchema = z.object({
firstName: z
.string()
.min(3, '[Zod] You must have a length of at least 3')
.startsWith('A', "[Zod] First name must start with 'A'"),
lastName: z.string().min(3, '[Zod] You must have a length of at least 3'),
})
export default function App() {
const form = useForm({
validators: {
onChange: ZodSchema,
},
})
return (
///
)
}
https://github.com/user-attachments/assets/b42e6208-6650-4166-9b48-cb0e2ccf180d
Initially, typing in either of the inputs will cause both of them to re-render on each keystroke. This is because the onChange validator seemingly triggers on each keystroke, and each field which contains an error will be re-rendered. I speculate this is because the internal fieldMeta gets updated, and that's causing this issue. But as soon as any of the inputs no longer contains invalid data, it stops getting re-rendered.
That would be okay in some scenarios, but what if you want to trigger validation only on fields that have isDirty: true state, or in other words, fields that users have already interacted with. In most cases it makes no sense to validate and display errors if the user have provided no input yet.
Most importantly, this particular behavior isn't documented anywhere. I basically spent several hours trying to pinpoint the exact issue until I was able to figure out what is going on.
Here is where things get extra confusing. Let's try to mimic the zod validation by writing our own simple validator like this:
export default function App() {
const form = useForm({
validators: {
onChange: ({ value }) => {
return {
fields: {
...value.firstName.length < 3 && {
firstName: '[Custom] You must have a length of at least 3'
},
...!value.firstName.startsWith('A') && {
firstName: "[Custom] First name must start with 'A'"
},
...value.lastName.length < 3 && {
lastName: '[Custom] You must have a length of at least 3'
},
}
}
}
},
})
return (
//
)
}
Now the problem is gone. All we ever did was remove the nested message property from the error map and return the error as string instead. Why that works? I have no clue.
https://github.com/user-attachments/assets/6ee74678-43a1-4d39-8ed3-60380aa692cb
Your minimal, reproducible example
https://codesandbox.io/p/devbox/gallant-stitch-5sqhyv
Steps to reproduce
Reproduction examples shown above.
Expected behavior
Form-level validators onChange validators should not cause all elements to re-render, and/or there should be an option to configure the default behavior. Most importantly, this should be documented somewhere, and if it's the intended behavior then it should be listed as a limitation.
Currently on crossroads not knowing what to do and if I should go back to RHF because this is kind of a deal-breakers for my particular use case.
Cheers.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
- OS: Linux
- Browser: Firefox/Chromium
TanStack Form adapter
react-form
TanStack Form version
1.14.2
TypeScript version
No response
Additional context
No response
Hey, @virtuallyunknown!
I'm facing the same issue currently. Have you found a workaround for this?
@Miteg you could try the workaround I've described in my post above, but I am not sure what the potential implications of that are to be honest, you'd have to further test this yourself.
Personally I ran into other issues as well, and the lack of in-depth documentation made me move back to RHF.
const dataSchema = {
title: z.string().min(3, "must have at least 3 characters"),
address: z.string().min(3, "must have at least 3 characters"),
};
const form = useForm({
defaultValues: {
title: "",
address: "",
},
validators: {
onChange: () => dataSchema,
},
onSubmit: ({ value }) => {
console.log(value);
},
});
guys I found a solution, just be cautious not to use z.object but just use normal object and return it on validators onChange fn
Hi, just to mention that I've been having the same issue when using a Yup schema at the form level, in this case using the blurAsync validator method.
Due to linked fields validation and other considerations, defining the validation schema at each field level is not viable, but equally showing errors for fields that have not even been touched yet is not good UX. I have tweaked the actual error component display to only show after field is touched, but the existence of any errors causes the component to re-render regardless.
The workaround by above doesn't work for yup. Would be good to have a native solution to this, as it doesn't seem like a particularly niche approach and it reduces the isolated rendering benefits of using TSF.
Hey, just my two cents, I was also surprised that when setting up validation at the form level:

const form = useForm({
validationLogic: revalidateLogic({ mode: 'blur', modeAfterSubmission: 'change' }),
})
Once you interact with a field, validation triggers for every field. Coming from React Hook Form, I expected it to only trigger for the field you actually touched.
But I got back the RHF behavior by setting up validators per field instead of at the form level:

<form.AppField
name="book"
validators={{
onDynamic: bookSchema,
}}
/>

In the end, didn't find it too much of a hassle, but I get where OP's coming from—I was also tripped up at first since it's not really documented (or at least wasn't clear to me in the docs).