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/resolversimport { 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’
Fieldacceptserroras astring | boolean | ReactNodeand renders it withrole="alert", whichreact-hook-form’serrors.<field>?.messageplugs straight into. - Cross-field rules. Use
z.refine/superRefinewhen one field’s validity depends on another (e.g. confirming a password) — the resolver forwards the errors to whichever path you set withctx.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