javascript - Why isn't my Zod validation error displaying in React Hook Form when validating an array of strings? - Stac

admin2025-05-01  2

I'm using React Hook Form with Zod validation to implement an OTP (One-Time Password) form where users input a 6-digit OTP, with each digit in a separate input field. I'm using z.object to validate that the OTP consists of exactly 6 numeric digits, and I expect validation errors to display when incorrect input is provided or when less than 6 input is submitted .

Here's my otpSchema and the relevant code:

const otpSchema = z.object({
  otp: z
    .array(
      z.string().regex(/^\d$/, 'Each input must be a single digit'), // Ensure it is a number
    )
    .length(6, 'OTP must consist of exactly 6 digits')
    .nonempty({
      message: "Can't be empty!",
    }),
});

const {
  register: verifyOtpRegister, //unused
  handleSubmit: verifyOtpSubmit,
  trigger: verifyOtpTrigger,
  control,
  getValues: getVerifyOtpValues,
  formState: { errors: verifyOtpErrors },
} = useForm({
  resolver: zodResolver(otpSchema),
});

const onVerifyOtp = (data?: any) => {
  console.log({ error: verifyOtpErrors.otp });
  console.log({ data });
  verifyOtp();
};

I'm rendering 6 separate input fields for the OTP, like this:

{[...Array(6)].map((_, index) => (
  <Grid item key={index}>
    <Controller
      name={`otp[${index}]`}
      control={control}
      render={({ field }) => (
        <TextField
          {...field}
          id={`otp-input-${index}`}
          inputMode="numeric"
          inputProps={{
            maxLength: 1,
            pattern: '[0-9]*',
          }}
          onChange={(e) => {
            console.log(e);
            handleInputChange(e, index, field);
          }}
          onKeyDown={(e) => handleKeyDown(e, index)}
          value={field.value || ''}
          autoComplete="off"
          sx={{ width: 40, textAlign: 'center' }}
        />
      )}
    />
  </Grid>
))}

<FormHelperText error>
  {`${verifyOtpErrors?.otp?.message || ''}`}
</FormHelperText>

<button
  type="submit"
  className="bg-blue-500 text-white py-2 px-4 rounded mt-4"
  onClick={() => {
    console.log(verifyOtpErrors?.otp);
    verifyOtpSubmit(onVerifyOtp);
  }}
>
  Verify OTP
</button>

The issue:

  • When I input the correct valid OTP format (e.g., 123456), the flow works, and the form successfully validates and submits.
  • However, when I enter invalid data (e.g., adb123, empty fields, or less/more than 6 digits), the flow doesn't work as expected but validation doesn't display any error messages. The errors.otp object is logged as undefined, and no error is shown in the FormHelperText.

What I've tried:

  • Added a console.log(verifyOtpErrors?.otp) in the onVerifyOtp function to debug, but verifyOtpErrors?.otp is always undefined for invalid input. Used verifyOtpTrigger('otp') to manually trigger validation, but it still doesn't populate the error messages and is undefined. Ensured that the name in Controller matches the schema (otp[index]).

I'm using React Hook Form with Zod validation to implement an OTP (One-Time Password) form where users input a 6-digit OTP, with each digit in a separate input field. I'm using z.object to validate that the OTP consists of exactly 6 numeric digits, and I expect validation errors to display when incorrect input is provided or when less than 6 input is submitted .

Here's my otpSchema and the relevant code:

const otpSchema = z.object({
  otp: z
    .array(
      z.string().regex(/^\d$/, 'Each input must be a single digit'), // Ensure it is a number
    )
    .length(6, 'OTP must consist of exactly 6 digits')
    .nonempty({
      message: "Can't be empty!",
    }),
});

const {
  register: verifyOtpRegister, //unused
  handleSubmit: verifyOtpSubmit,
  trigger: verifyOtpTrigger,
  control,
  getValues: getVerifyOtpValues,
  formState: { errors: verifyOtpErrors },
} = useForm({
  resolver: zodResolver(otpSchema),
});

const onVerifyOtp = (data?: any) => {
  console.log({ error: verifyOtpErrors.otp });
  console.log({ data });
  verifyOtp();
};

I'm rendering 6 separate input fields for the OTP, like this:

{[...Array(6)].map((_, index) => (
  <Grid item key={index}>
    <Controller
      name={`otp[${index}]`}
      control={control}
      render={({ field }) => (
        <TextField
          {...field}
          id={`otp-input-${index}`}
          inputMode="numeric"
          inputProps={{
            maxLength: 1,
            pattern: '[0-9]*',
          }}
          onChange={(e) => {
            console.log(e);
            handleInputChange(e, index, field);
          }}
          onKeyDown={(e) => handleKeyDown(e, index)}
          value={field.value || ''}
          autoComplete="off"
          sx={{ width: 40, textAlign: 'center' }}
        />
      )}
    />
  </Grid>
))}

<FormHelperText error>
  {`${verifyOtpErrors?.otp?.message || ''}`}
</FormHelperText>

<button
  type="submit"
  className="bg-blue-500 text-white py-2 px-4 rounded mt-4"
  onClick={() => {
    console.log(verifyOtpErrors?.otp);
    verifyOtpSubmit(onVerifyOtp);
  }}
>
  Verify OTP
</button>

The issue:

  • When I input the correct valid OTP format (e.g., 123456), the flow works, and the form successfully validates and submits.
  • However, when I enter invalid data (e.g., adb123, empty fields, or less/more than 6 digits), the flow doesn't work as expected but validation doesn't display any error messages. The errors.otp object is logged as undefined, and no error is shown in the FormHelperText.

What I've tried:

  • Added a console.log(verifyOtpErrors?.otp) in the onVerifyOtp function to debug, but verifyOtpErrors?.otp is always undefined for invalid input. Used verifyOtpTrigger('otp') to manually trigger validation, but it still doesn't populate the error messages and is undefined. Ensured that the name in Controller matches the schema (otp[index]).
Share Improve this question asked Jan 2 at 17:33 VinVin 336 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

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 }),
};
转载请注明原文地址:http://www.anycun.com/QandA/1746104896a91736.html