Skip to Content
Docs🧩 ⏐ RecipesForms

Forms

reablocks recommends pairing react-hook-form  with Zod  for form state and validation. react-hook-form keeps form state and rendering performant; Zod gives you a single schema that defines both the runtime validation and the inferred TypeScript type for the form values.

Here is a basic login form example:



Wiring react-hook-form to an Input

Wrap the reablocks Input in a Controller so react-hook-form owns the field state:

import { Input } from 'reablocks'; import { useForm, Controller } from 'react-hook-form'; export const BasicForm = () => { const { control, handleSubmit, formState: { isSubmitting } } = useForm(); return ( <form onSubmit={handleSubmit(values => console.log('values', values))}> <Controller name="email" control={control} render={({ field: { value, onBlur, onChange } }) => ( <Input name="email" disabled={isSubmitting} placeholder="Enter your email address..." value={value} type="email" onChange={onChange} onBlur={onBlur} /> )} /> </form> ); };

Adding validation with Zod

Define a Zod schema for the form, infer the TypeScript type from it, and plug it into react-hook-form via @hookform/resolvers/zod. The resolver runs the schema on every submit (and on change/blur if you opt in via mode) and surfaces errors through formState.errors.

npm install zod @hookform/resolvers
import { Field, Input, Button } from 'reablocks'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const loginSchema = z.object({ email: z.string().email('Enter a valid email address'), password: z.string().min(8, 'Password must be at least 8 characters') }); type LoginValues = z.infer<typeof loginSchema>; export const LoginForm = () => { const { control, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginValues>({ resolver: zodResolver(loginSchema), defaultValues: { email: '', password: '' } }); return ( <form onSubmit={handleSubmit(values => console.log(values))}> <Controller name="email" control={control} render={({ field }) => ( <Field label="Email" error={errors.email?.message ?? false}> <Input {...field} type="email" placeholder="you@example.com" disabled={isSubmitting} error={!!errors.email} /> </Field> )} /> <Controller name="password" control={control} render={({ field }) => ( <Field label="Password" error={errors.password?.message ?? false}> <Input {...field} type="password" disabled={isSubmitting} error={!!errors.password} /> </Field> )} /> <Button type="submit" disabled={isSubmitting}>Sign in</Button> </form> ); };

A few notes on this pattern:

  • Single source of truth. z.infer<typeof loginSchema> derives the form value type, so renaming a field in the schema updates the type, the resolver, and the editor’s autocomplete in one go.
  • Field-level error rendering. reablocks’ Field accepts error as a string | boolean | ReactNode and renders it with role="alert", which react-hook-form’s errors.<field>?.message plugs straight into.
  • Cross-field rules. Use z.refine / superRefine when one field’s validity depends on another (e.g. confirming a password) — the resolver forwards the errors to whichever path you set with ctx.addIssue.

More

  • react-hook-form  — full API reference.
  • Zod  — schema definitions, parsing, and refinements.
  • @hookform/resolvers — also provides resolvers for Yup, Joi, Valibot, and others if Zod isn’t your stack.
Last updated on