| name | form-patterns |
| description | Form handling with React Hook Form and Zod validation |
Form Patterns
1. Basic Form with Validation
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
type LoginFormData = z.infer<typeof loginSchema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
await login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
{...register("email")}
type="email"
id="email"
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
{...register("password")}
type="password"
id="password"
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
{errors.password && (
< =>{errors.password.message}
)}
{isSubmitting ? "Signing in..." : "Sign in"}
);
}
2. Complex Schema Validation
const userSchema = z.object({
name: z.string().min(2, "Name is too short"),
email: z.string().email(),
age: z.number().min(18, "Must be 18 or older").max(120),
newsletter: z.boolean().default(false),
role: z.enum(["user", "admin", "moderator"]),
address: z.object({
street: z.string().min(1),
city: z.string().min(1),
zip: z.string().regex(/^\d{5}$/, "Invalid ZIP code"),
}),
tags: z.array(z.string()).min(1, "Add at least one tag"),
: z.().(),
: z.(),
}).( data. === data., {
: ,
: [],
});
3. Reusable Form Field Component
import { useFormContext, FieldPath, FieldValues } from "react-hook-form";
interface FormFieldProps<T extends FieldValues> {
name: FieldPath<T>;
label: string;
type?: "text" | "email" | "password" | "number";
placeholder?: string;
}
export function FormField<T extends FieldValues>({
name,
label,
type = "text",
placeholder,
}: FormFieldProps<T>) {
const {
register,
formState: { errors },
} = useFormContext<T>();
const error = errors[name];
return (
<div className="space-y-1">
<label htmlFor={name} className="block text-sm font-medium">
{label}
</label>
<input
{...register(name)}
type={type}
id={name}
=
=
" ",
&& " "
)}
/>
{error && (
{error.message as string}
)}
);
}
() {
methods = useForm<>({ : (schema) });
(
);
}
4. Dynamic Form Fields (Array)
import { useFieldArray, useForm } from "react-hook-form";
const schema = z.object({
users: z.array(z.object({
name: z.string().min(1),
email: z.string().email(),
})).min(1),
});
type FormData = z.infer<typeof schema>;
function DynamicForm() {
const { control, register, handleSubmit } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
users: [{ name: "", email: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "users",
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-4">
<input {(`${}`)} = />
remove(index)}>
Remove
))}
append({ name: "", email: "" })}>
Add User
Submit
);
}
5. Form with File Upload
const schema = z.object({
name: z.string().min(1),
avatar: z
.instanceof(FileList)
.refine((files) => files.length > 0, "Avatar is required")
.refine(
(files) => files[0]?.size <= 5 * 1024 * 1024,
"File must be less than 5MB"
)
.refine(
(files) => ["image/jpeg", "image/png"].includes(files[0]?.type),
"Only JPEG or PNG allowed"
),
});
function FileUploadForm() {
const { register, handleSubmit, watch, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
const avatar = watch("avatar");
const preview = avatar?.[0] ? URL.createObjectURL(avatar[0]) : null;
return (
);
}
6. Server Actions (Next.js)
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
});
export async function subscribeAction(formData: FormData) {
const result = schema.safeParse({
email: formData.get("email"),
});
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
await subscribeToNewsletter(result.data.email);
return { success: true };
}
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { subscribeAction } from "./actions";
function SubmitButton() {
const { pending } = useFormStatus();
return (
< = =>
{pending ? "Subscribing..." : "Subscribe"}
);
}
() {
[state, formAction] = (subscribeAction, );
(
);
}
Best Practices
- Always use Zod for schema validation
- Show errors inline next to the field
- Disable submit while submitting
- Use
FormProvider for deep nesting
- Debounce async validation (username availability)
- Reset form after successful submission