user1779418
user1779418

Reputation: 495

react-hook-form with zod not returning all errors on submit

I am using Next.js 15.1.6, zod 3.24.2, and react-hook-form 7.52.2. I'm creating a form that has a bunch of d&d character sheet fields, and I can't seem to get the form submission validation to work right - on form submit, if I leave all fields blank, the errors object is empty. If I click submit a second time, the error for Level is added to the object. And that's it. I'd expect the errors object to hold errors for all mandatory fields (such as number greater than zero, dropdown selections, text field filled out).

The useForm() usage looks like this:

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(schema),
    mode: 'onBlur',
    defaultValues: schemaDefaults
  });

zod's schema looks like this:

const invalidNumberError = 'A number is required'

const pureNumberValidation = z.number({ invalid_type_error: invalidNumberError });
const scoreValidation = z.number({ invalid_type_error: invalidNumberError }).positive().min(1).max(30);
const positiveNumberValidation = z.number({ invalid_type_error: invalidNumberError }).positive();

export const schema = z.object({
  name: z.string().nonempty({ message: 'Name required' }),
  class: pureNumberValidation.min(1, { message: 'Class required' }),
  level: positiveNumberValidation.max(20),
  hp: positiveNumberValidation,
  ac: positiveNumberValidation,
  speed: positiveNumberValidation,
  initiative: positiveNumberValidation,
  proficiencyBonus: positiveNumberValidation,
  race: pureNumberValidation.min(1, { message: 'Race required' }),
  alignment: pureNumberValidation.min(1, { message: 'Alignment required' }),
  constitutionScore: scoreValidation,
  constitutionProficiencyBonus: pureNumberValidation,
  charismaScore: scoreValidation,
  charismaProficiencyBonus: pureNumberValidation,
  dexterityScore: scoreValidation,
  dexterityProficiencyBonus: pureNumberValidation,
  intelligenceScore: scoreValidation,
  intelligenceProficiencyBonus: pureNumberValidation,
  strengthScore: scoreValidation,
  strengthProficiencyBonus: pureNumberValidation,
  wisdomScore: scoreValidation,
  wisdomProficiencyBonus: pureNumberValidation,
  acrobatics: pureNumberValidation,
  animalhandling: pureNumberValidation,
  arcana: pureNumberValidation,
  athletics: pureNumberValidation,
  deception: pureNumberValidation,
  history: pureNumberValidation,
  insight: pureNumberValidation,
  intimidation: pureNumberValidation,
  investigation: pureNumberValidation,
  medicine: pureNumberValidation,
  nature: pureNumberValidation,
  perception: pureNumberValidation,
  performance: pureNumberValidation,
  persuasion: pureNumberValidation,
  religion: pureNumberValidation,
  sleightofhand: pureNumberValidation,
  stealth: pureNumberValidation,
  survival: pureNumberValidation
});

If I just do a console.log(errors) in the react component, on submit the first time the object isn't logged because nothing has changed. On submit the second time, the object looks like the below. And on subsequent on submits, nothing is changed/logged.

{level: {…}}
level:
  message: "Number must be greater than 0"
  ref: input#level-input.<tailwind css classes>
  type: "too_small"

errors object with level

The error handling works on blur though - if I enter and then leave a field while it has invalid data, such as not selecting an option on the Class dropdown, or setting a negative number on HP, or setting AC to 0 - that produces the correct error object.

There are different types of inputs, but for example this is how the reusable numeric input jsx is defined:

   <div>
      <label
        id={id}
        htmlFor={`${id}-input`}
        aria-label={label}
        className="<classes>"
      >
        {label}
      </label>
      <input
        type="number"
        id={`${id}-input`}
        {...register(name, { valueAsNumber: true })}
        min={min}
        max={max}
        aria-invalid={errors[name]?.message ? true : false}
        className='<classes>'
      />
      {errors[name]?.message && <ErrorDisplay message={errors[name].message} />}
    </div>

Upvotes: 0

Views: 37

Answers (0)

Related Questions