Cursor can generate verbose, deeply nested React form code with manual state management for every field. By referencing react-hook-form or similar libraries in your .cursor/rules/ file, providing schema examples with @file, and prompting with specific form structure requirements, you get Cursor to produce clean, maintainable forms with validation, error handling, and minimal boilerplate code.
Simplifying complex UI logic with Cursor
Complex forms with nested objects, dynamic arrays, and conditional fields are one of the hardest UI patterns to get right. Cursor often generates forms with individual useState calls for every field, manual onChange handlers, and inline validation. This tutorial shows how to guide Cursor toward react-hook-form with Zod validation, producing forms that are shorter, type-safe, and easier to maintain.
Prerequisites
- Cursor installed with a React/TypeScript project
- react-hook-form and @hookform/resolvers installed
- Zod installed for schema validation
- Basic understanding of React forms and hooks
Step-by-step guide
Create a form generation rule for Cursor
Create a form generation rule for Cursor
Add a project rule that tells Cursor to use react-hook-form instead of manual state management. Specify the exact libraries and patterns to use, and explicitly forbid the manual patterns that create boilerplate.
1---2description: Use react-hook-form + Zod for all form generation3globs: "*.tsx,*.jsx"4alwaysApply: true5---67# Form Rules8- ALWAYS use react-hook-form for form state management9- ALWAYS use Zod schemas with @hookform/resolvers/zod for validation10- NEVER use individual useState calls for form fields11- NEVER write manual onChange handlers for form inputs12- NEVER use uncontrolled inputs without register()13- Use useFieldArray for dynamic lists of fields14- Use Controller for third-party components (Select, DatePicker)15- Define Zod schema first, then infer TypeScript types from it16- Create reusable field wrapper components for consistent styling1718## Pattern:19```typescript20const schema = z.object({ name: z.string().min(1) });21type FormData = z.infer<typeof schema>;22const { register, handleSubmit, formState: { errors } } = useForm<FormData>({23 resolver: zodResolver(schema),24});25```Expected result: Cursor generates forms using react-hook-form with Zod validation instead of manual state management.
Generate a complex nested form with a single prompt
Generate a complex nested form with a single prompt
Open Chat with Cmd+L and describe the form structure. Include nested objects, arrays, and conditional fields. Reference the rule and your UI component library so Cursor uses the correct components.
1@react-forms.mdc @src/components/ui/23Create an order form with these fields:41. Customer section: name (required), email (required, valid email), phone (optional)52. Shipping address: street, city, state, zip (all required)63. Items array (dynamic, add/remove): productId (select), quantity (number, min 1), notes (optional)74. Payment: method (radio: credit/debit/paypal), card number (conditional, shown only for credit/debit)89Use react-hook-form with Zod schema. Use useFieldArray for items.10Use Controller for select and radio inputs.11Infer TypeScript types from the Zod schema.Pro tip: Describe the form structure hierarchically in your prompt. Cursor handles nested forms better when the nesting is clear in the prompt itself.
Expected result: Cursor generates a complete form component with Zod schema, useForm, useFieldArray for items, and Controller for custom inputs.
Create reusable field components for Cursor to use
Create reusable field components for Cursor to use
Build a small library of reusable form field components that wrap react-hook-form's register and error display. When Cursor sees these via @file, it generates forms using your components instead of raw HTML inputs with inline error handling.
1import { FieldError, UseFormRegisterReturn } from 'react-hook-form';23interface FormFieldProps {4 label: string;5 registration: UseFormRegisterReturn;6 error?: FieldError;7 type?: string;8 placeholder?: string;9}1011export const FormField = ({ label, registration, error, type = 'text', placeholder }: FormFieldProps) => (12 <div className="space-y-1">13 <label className="block text-sm font-medium text-gray-700">{label}</label>14 <input15 type={type}16 {...registration}17 placeholder={placeholder}18 className={`w-full rounded-md border px-3 py-2 text-sm ${19 error ? 'border-red-500' : 'border-gray-300'20 }`}21 />22 {error && <p className="text-sm text-red-600">{error.message}</p>}23 </div>24);2526export const FormSelect = ({ label, registration, error, options }: FormFieldProps & { options: { value: string; label: string }[] }) => (27 <div className="space-y-1">28 <label className="block text-sm font-medium text-gray-700">{label}</label>29 <select {...registration} className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm">30 <option value="">Select...</option>31 {options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}32 </select>33 {error && <p className="text-sm text-red-600">{error.message}</p>}34 </div>35);Expected result: Reusable form components that Cursor imports when generating forms, eliminating repeated input and error display markup.
Refactor an existing verbose form with Cmd+K
Refactor an existing verbose form with Cmd+K
Select an existing form that uses multiple useState calls and manual handlers. Press Cmd+K and prompt Cursor to refactor it to react-hook-form. This is the fastest way to modernize legacy form code.
1Refactor this form to use react-hook-form with Zod validation.2Replace all useState calls with useForm register.3Replace manual onChange handlers with react-hook-form's register().4Create a Zod schema matching the current validation logic.5Infer the TypeScript type from the schema.6Use @/components/form/FormField for input rendering.Expected result: The verbose form is rewritten to use react-hook-form, reducing the code by 40-60% while maintaining all validation and behavior.
Generate form submission handling with error states
Generate form submission handling with error states
Ask Cursor to add proper submission handling including loading states, server error display, and success feedback. Reference the form component you just created so Cursor adds to it rather than regenerating.
1@react-forms.mdc @src/components/OrderForm.tsx23Add submission handling to this form:41. Show a loading spinner on the submit button during submission52. Call POST /api/orders with the form data63. Display server validation errors next to the relevant fields74. Show a success toast on successful submission85. Reset the form after successful submission96. Handle network errors with a general error banner1011Use react-hook-form's setError() for server-side field errors.12Use the form's isSubmitting state for the loading indicator.Expected result: Cursor adds submission handling with loading states, server error mapping, success feedback, and form reset.
Complete working example
1import { z } from 'zod';23const addressSchema = z.object({4 street: z.string().min(1, 'Street is required'),5 city: z.string().min(1, 'City is required'),6 state: z.string().min(2, 'State is required').max(2),7 zip: z.string().regex(/^\d{5}(-\d{4})?$/, 'Invalid zip code'),8});910const orderItemSchema = z.object({11 productId: z.string().min(1, 'Select a product'),12 quantity: z.number().int().min(1, 'Minimum quantity is 1'),13 notes: z.string().max(500).optional(),14});1516export const orderFormSchema = z.object({17 customer: z.object({18 name: z.string().min(1, 'Name is required').max(100),19 email: z.string().email('Invalid email address'),20 phone: z.string().optional(),21 }),22 shippingAddress: addressSchema,23 items: z.array(orderItemSchema).min(1, 'Add at least one item'),24 payment: z.object({25 method: z.enum(['credit', 'debit', 'paypal']),26 cardNumber: z.string().optional(),27 }).refine(28 (data) => {29 if (data.method === 'credit' || data.method === 'debit') {30 return data.cardNumber && data.cardNumber.length >= 13;31 }32 return true;33 },34 { message: 'Card number is required', path: ['cardNumber'] }35 ),36});3738export type OrderFormData = z.infer<typeof orderFormSchema>;Common mistakes when simplifying Complex UI Logic with Cursor
Why it's a problem: Not specifying react-hook-form in rules and getting useState forms
How to avoid: Add ALWAYS use react-hook-form and NEVER use individual useState for form fields to your .cursor/rules/ file.
Why it's a problem: Defining TypeScript types separately from Zod schemas
How to avoid: Add to your rules: ALWAYS infer TypeScript types from Zod schemas using z.infer. NEVER define form types separately.
Why it's a problem: Using Controller for simple text inputs
How to avoid: Specify in rules: Use register() for native HTML inputs. Use Controller only for third-party components like DatePicker or custom Select.
Best practices
- Define Zod schemas first and infer TypeScript types with z.infer
- Use useFieldArray for dynamic form sections instead of manual array state
- Create reusable FormField components so Cursor does not duplicate input markup
- Reference your Zod schema file with @file when asking Cursor to generate the form component
- Use react-hook-form's setError for server-side validation error mapping
- Keep form schemas in separate files from components for better reusability
- Test generated forms with edge cases like empty submissions and maximum-length inputs
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Generate a React order form using react-hook-form with Zod validation. Include nested customer info, shipping address, dynamic item array with add/remove, and conditional payment fields. Use TypeScript and infer types from the Zod schema.
@react-forms.mdc @src/schemas/order-form.ts @src/components/form/FormField.tsx Generate a React form component for the order schema. Use react-hook-form with zodResolver, useFieldArray for items, Controller for select inputs, and FormField for text inputs. Show loading and error states for submission.
Frequently asked questions
Should I use react-hook-form or Formik?
React-hook-form is recommended for Cursor workflows because it uses register() patterns that are simpler and less verbose than Formik's Field components. Cursor generates cleaner code with react-hook-form.
How do I handle file uploads in Cursor-generated forms?
Use Controller to wrap the file input and store the File object in form state. Add a separate Zod schema for file validation (type, size). Reference a file upload example in your rules.
Can Cursor generate multi-step wizard forms?
Yes. Prompt Cursor with the step structure and specify that form state should persist across steps using react-hook-form's getValues and trigger for per-step validation.
How do I pre-populate form fields for edit mode?
Pass defaultValues to useForm and mention edit mode in your prompt. Cursor will generate useEffect to fetch existing data and reset the form with the fetched values.
What about form performance with many fields?
React-hook-form uses uncontrolled inputs by default, which avoids re-renders on every keystroke. For extremely large forms, add a rule specifying to use lazy field registration.
Can RapidDev help build complex form systems?
Yes. RapidDev designs form architectures with dynamic validation, multi-step wizards, and server-side error handling, and configures Cursor rules to generate forms that match your design system.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation