create-form
Set up hybrid client/server validated forms with Zod and React Hook Form. Use when creating forms with validation.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Create Form
When to Use
- Creating any form with validation
- User asks to "add a form" or "create a form"
Critical Rule
// CORRECT - Use <form> with manual fetcher.submit()
<form onSubmit={handleSubmit(onSubmit)}>
// WRONG - Causes submission conflicts
<fetcher.Form onSubmit={handleSubmit(onSubmit)}>
Core Pattern
- Same Zod schema on client and server
- Client: Instant feedback with React Hook Form
-
Server: Security with
validateFormData() - Auto error sync: Server errors populate form fields
Quick Start
1. Schema (app/lib/validations.ts)
import { z } from 'zod';
export const contactFormSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
});
export type ContactFormData = z.infer<typeof contactFormSchema>;
2. Server Action
import { validateFormData } from '~/lib/form-validation.server';
import { zodResolver } from '@hookform/resolvers/zod';
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const { data, errors } = await validateFormData<ContactFormData>(
formData,
zodResolver(contactFormSchema)
);
if (errors) return data({ errors }, { status: 400 });
await processData(data!);
return redirect('/success');
}
3. Client Form
import { useFetcher } from 'react-router';
import { useValidatedForm } from '~/lib/form-hooks';
export default function ContactPage() {
const fetcher = useFetcher();
const { register, handleSubmit, formState: { errors } } = useValidatedForm({
resolver: zodResolver(contactFormSchema),
errors: fetcher.data?.errors,
});
const onSubmit = (data: ContactFormData) => {
const formData = new FormData();
formData.append('name', data.name);
fetcher.submit(formData, { method: 'POST' });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<TextInput {...register('name')} error={errors.name?.message} />
<Button type="submit">Submit</Button>
</form>
);
}
Checklist
- Define Zod schema in
app/lib/validations.ts - Add server action with
validateFormData() - Use
useValidatedFormhook in component - Use
<form>(not<fetcher.Form>) withhandleSubmit
Templates
Full Reference
See .github/instructions/form-validation.instructions.md for:
- Field-level vs form-level errors
- Conditional validation
- File uploads
- Complex schema patterns
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!