I have a form that uses both react-hook-form and Zod for validation. the schema looks something like this:
const schema = z
.object({
email: z.string().email().optional(),
userName: z.string().optional(),
contactType: z.string(),
})
.superRefine((data, ctx) => {
if (data.contactType === 'email' && !data.email) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['email'],
message: 'email is required',
});
}else if {//more fields here}
});
What usually happens with react-hook-form and Zod, is the following:
- You can type what you want, even if it's a mistake, and validation is turned off
- The moment you submit the form, validation occurs.
- If there are any wrong fields, the error object is filled
- the error handling mode changes to "onChange", meaning you're now gonna be notified of every error change, even if you hadn't hit submit
This is a great behavior and I wanna keep it, but the problem is, my error is defined in the "superRefine" method, and when this happens, it no longer updates dynamically (step 4 doesn't work). I only see the error message change when I press submit again.
What I'm trying to achieve here, is to have a field required only if the contactType matches, so if contactType is email, require email, if contactType is userName, require userName. if I submit when email is empty (for example), it tells me "email is required". but once I change contactType to userName, I want to see "userName" is required straight away.
EDIT: I've added "unregister" to the field that I remove, and it correctly shows my errors, but now something else happens when I do the following:
- Write something in input, when type set to "email" (for example, write "123")
- Change type to username Now, if I look at the form state, email is removed, but the data inside is my correct type, with the old string. (for example, username: "123")
I assume this is because I have one input, with a dynamic register
{...register('user.${idx}.${fieldName}')}
(where 'fieldName' is derived from 'watch')
I assume register happens before watch, and then quickly sets the new data to what I have in the input, before deleting the input.
I can create a few inputs that are rendered conditionally, each for the specific 'contactType' but I wonder if there's a better way
I have a form that uses both react-hook-form and Zod for validation. the schema looks something like this:
const schema = z
.object({
email: z.string().email().optional(),
userName: z.string().optional(),
contactType: z.string(),
})
.superRefine((data, ctx) => {
if (data.contactType === 'email' && !data.email) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['email'],
message: 'email is required',
});
}else if {//more fields here}
});
What usually happens with react-hook-form and Zod, is the following:
- You can type what you want, even if it's a mistake, and validation is turned off
- The moment you submit the form, validation occurs.
- If there are any wrong fields, the error object is filled
- the error handling mode changes to "onChange", meaning you're now gonna be notified of every error change, even if you hadn't hit submit
This is a great behavior and I wanna keep it, but the problem is, my error is defined in the "superRefine" method, and when this happens, it no longer updates dynamically (step 4 doesn't work). I only see the error message change when I press submit again.
What I'm trying to achieve here, is to have a field required only if the contactType matches, so if contactType is email, require email, if contactType is userName, require userName. if I submit when email is empty (for example), it tells me "email is required". but once I change contactType to userName, I want to see "userName" is required straight away.
EDIT: I've added "unregister" to the field that I remove, and it correctly shows my errors, but now something else happens when I do the following:
- Write something in input, when type set to "email" (for example, write "123")
- Change type to username Now, if I look at the form state, email is removed, but the data inside is my correct type, with the old string. (for example, username: "123")
I assume this is because I have one input, with a dynamic register
{...register('user.${idx}.${fieldName}')}
(where 'fieldName' is derived from 'watch')
I assume register happens before watch, and then quickly sets the new data to what I have in the input, before deleting the input.
I can create a few inputs that are rendered conditionally, each for the specific 'contactType' but I wonder if there's a better way
Share Improve this question edited Feb 7, 2024 at 17:33 Ido Kadosh asked Feb 6, 2024 at 16:27 Ido KadoshIdo Kadosh 1452 silver badges12 bronze badges2 Answers
Reset to default 5Discriminated Union:
const contactTypeEmailSchema = z.object({
email: z.string().email(),
contactType: z.literal('email'),
})
const contactTypeUserNameSchema = z.object({
userName: z.string(),
contactType: z.literal('userName'),
})
const schema = z.discriminatedUnion('contactType', [
contactTypeEmailSchema,
contactTypeUserNameSchema
])
No superrefine/custom logic needed, just clean schemas
const schema = z
.object({
email: z.string().email().optional(),
userName: z.string().optional(),
contactType: z.string(),
})
.refine((data) =>
data.contactType === 'email' && !data.email) ?
z.string().nonempty() : z.unknown()
});