To ensure your validation works as expected, I recommend using the refine method in your validation schema. This approach allows you to implement more complex and customized validation techniques. Also instead of manually triggering validation with verifyOtpTrigger('otp'), it's generally more efficient to use handleSubmit for form validation.
Here’s an example of how you can implement basic otp form:
import { NumericFormat } from "react-number-format";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { Button, FormHelperText, Grid, TextField } from "@mui/material";
import { defaultValues, otpSchema, OtpValues } from "./otp-form.configs";
export const OtpForm = () => {
const form = useForm<OtpValues>({
defaultValues,
resolver: zodResolver(otpSchema),
});
const { fields } = useFieldArray<OtpValues>({
control: form.control,
name: "otp",
});
const errors = form.formState.errors;
const verifyOtpCode = (values: OtpValues): void => {
console.log(values);
};
return (
<form onSubmit={form.handleSubmit(verifyOtpCode)}>
<Grid container={true}>
{fields.map((field, index) => (
<Grid item={true} key={field.id}>
<Controller
name={`otp.${index}.value`}
control={form.control}
render={({ field: { ref, onChange, ...field } }) => (
<NumericFormat
customInput={TextField}
{...field}
inputRef={ref}
inputProps={{ maxLength: 1 }}
size="small"
onValueChange={({ floatValue }) =>
onChange(floatValue ?? null)
}
sx={{ width: 40 }}
/>
)}
/>
</Grid>
))}
</Grid>
{errors?.otp?.root && (
<FormHelperText error={true}>{errors.otp.root.message}</FormHelperText>
)}
<Button type="submit" variant="contained">
Verify OTP
</Button>
</form>
);
};
import { z } from "zod";
// TODO: move to the /shared/error-messages/otp.messages.ts
const OTP_CODE_INVALID = "Please provide a valid OTP code.";
export const otpSchema = z.object({
otp: z
.array(z.object({ value: z.number().nullable() }))
// Using refine is important here because we want to return only a single error message in the array of errors.
// Without it, we would receive individual errors for each of the 6 items in the array.
.refine((codes) => codes.every((code) => code.value !== null), OTP_CODE_INVALID),
});
export type OtpValues = z.infer<typeof otpSchema>;
export const defaultValues: OtpValues = {
otp: new Array(6).fill({ value: null }),
};