--- title: Build the Validation Schema Once, Outside the Render Path impact: HIGH impactDescription: stops rebuilding the whole schema object on every keystroke tags: 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):** ```typescript function InviteMemberForm() { const { register, handleSubmit } = useForm({ resolver: zodResolver( z.object({ email: z.email('Enter a valid email address'), role: z.enum(['admin', 'editor', 'viewer']), }), ), defaultValues: { email: '', role: 'viewer' }, }) return (
) } ``` **Correct (built once at module load):** ```typescript const inviteSchema = z.object({ email: z.email('Enter a valid email address'), role: z.enum(['admin', 'editor', 'viewer']), }) function InviteMemberForm() { const { register, handleSubmit } = useForm({ resolver: zodResolver(inviteSchema), defaultValues: { email: '', role: 'viewer' }, }) return (
) } ``` **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: ```typescript 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({ resolver: zodResolver(schema), defaultValues: { seats: 1 }, }) return (
) } ``` 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](https://github.com/react-hook-form/resolvers)