Files
gruperly/.agents/skills/react-hook-form/references/valid-resolver-caching.md
2026-09-04 16:49:24 -03:00

2.7 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Build the Validation Schema Once, Outside the Render Path HIGH stops rebuilding the whole schema object on every keystroke valid, resolver, schema, zod, useMemo

Build the Validation Schema Once, Outside the Render Path

There is no resolver cache in React Hook Form — useForm reassigns control._options = props on every render, and whatever resolver you passed is used as-is. The cost of an inline schema is the construction: z.object({ … }) allocates a fresh validator tree every render, and under mode: 'onChange' that is once per keystroke, on top of the validation itself. Hoisting the schema to module scope makes it a one-time cost at import.

Incorrect (a new schema object built on every render):

function InviteMemberForm() {
  const { register, handleSubmit } = useForm<InviteFormValues>({
    resolver: zodResolver(
      z.object({
        email: z.email('Enter a valid email address'),
        role: z.enum(['admin', 'editor', 'viewer']),
      }),
    ),
    defaultValues: { email: '', role: 'viewer' },
  })

  return (
    <form onSubmit={handleSubmit(sendInvite)}>
      <input {...register('email')} />
    </form>
  )
}

Correct (built once at module load):

const inviteSchema = z.object({
  email: z.email('Enter a valid email address'),
  role: z.enum(['admin', 'editor', 'viewer']),
})

function InviteMemberForm() {
  const { register, handleSubmit } = useForm<InviteFormValues>({
    resolver: zodResolver(inviteSchema),
    defaultValues: { email: '', role: 'viewer' },
  })

  return (
    <form onSubmit={handleSubmit(sendInvite)}>
      <input {...register('email')} />
    </form>
  )
}

When the schema genuinely depends on props or context, hoist a factory instead of the schema and memoize the call — so it rebuilds when the input changes, not when the component renders:

const createSeatSchema = (maxSeats: number) =>
  z.object({
    seats: z.number().int().max(maxSeats, `Your plan allows ${maxSeats} seats`),
  })

function SeatAllocationForm({ maxSeats }: { maxSeats: number }) {
  const schema = useMemo(() => createSeatSchema(maxSeats), [maxSeats])

  const { register, handleSubmit } = useForm<SeatFormValues>({
    resolver: zodResolver(schema),
    defaultValues: { seats: 1 },
  })

  return (
    <form onSubmit={handleSubmit(updateSeats)}>
      <input type="number" {...register('seats', { valueAsNumber: true })} />
    </form>
  )
}

Prefer a schema-level .refine() over a factory when the rule depends on other fields rather than on props — cross-field rules don't need the schema rebuilt.

Reference: React Hook Form Resolvers