{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"complex-forms-done-right-u2un5","url":"https://api.zyvop.com/complex-forms-done-right-u2un5","title":"Complex Forms Done Right","subtitle":null,"tldr":"At Motorola Solutions I built a multi-step contract wizard where users selected products, services, additional options, and pricing tiers — dozens of fields tha...","keywords":["Forms","React Hook Form","Architecture"],"entities":["Maksym Kuzmitskyi (MaximusFT)","Staff Frontend Engineer","Forms","React Hook Form","Architecture","ZyVOP"],"keyTakeaways":["At Motorola Solutions I built a multi-step contract wizard where users selected products, services, additional options, and pricing tiers — dozens of fields that depended on each other.","It was a live calculator that recalculated totals, toggled conditional sections, and validated business rules at every step.","Later, at an insurance company, I worked on a policy builder that asked everything from property type and vehicle model to how many pets you own and how long you've had them."],"headings":["The Challenge","The Right Tools","Architecture Patterns","1. Schema-First Design","2. Async Validation","3. Multi-Step Forms","4. Dynamic Field Arrays","5. Conditional Fields","Error Handling UX","Performance Optimization","File Uploads","Submission with TanStack Query","Accessibility Checklist","Testing","Conclusion"],"outboundLinks":["https://ma-x.im/contact"],"contentText":"At Motorola Solutions I built a multi-step contract wizard where users selected products, services, additional options, and pricing tiers — dozens of fields that depended on each other. It wasn't just a form. It was a live calculator that recalculated totals, toggled conditional sections, and validated business rules at every step. Later, at an insurance company, I worked on a policy builder that asked everything from property type and vehicle model to how many pets you own and how long you've had them. Every answer changed what came next. Every combination produced a different quote. Forms like these are where most implementations fall apart. A simple login form? Easy. A multi-step wizard with conditional fields, async validation, dynamic pricing, and file uploads? That's a different engineering problem entirely. The Challenge Complex forms have multiple concerns: State management - Field values, touched, dirty states Validation - Sync and async, field-level and form-level UX - When to show errors, loading states, success feedback Performance - Re-renders, large forms, dynamic fields Accessibility - Screen readers, keyboard navigation, error announcements The Right Tools After building dozens of complex forms, here's my stack: React Hook Form - Uncontrolled forms, great performance Zod - Schema validation, type inference TanStack Query - Async validation, mutations Radix UI - Accessible form primitives Architecture Patterns 1. Schema-First Design Define your form schema first: import { z } from \"zod\"; const addressSchema = z.object({ street: z.string().min(1, \"Street is required\"), city: z.string().min(1, \"City is required\"), country: z.string().min(1, \"Country is required\"), postalCode: z.string().regex(/^d{5}$/, \"Invalid postal code\"), }); const userSchema = z.object({ email: z.string().email(\"Invalid email\"), password: z .string() .min(8, \"At least 8 characters\") .regex(/[A-Z]/, \"Need uppercase letter\") .regex(/[0-9]/, \"Need a number\"), confirmPassword: z.string(), address: addressSchema, terms: z.literal(true, { errorMap: () =&gt; ({ message: \"You must accept terms\" }), }), }).refine((data) =&gt; data.password === data.confirmPassword, { message: \"Passwords don't match\", path: [\"confirmPassword\"], }); type UserFormData = z.infer&lt;typeof userSchema&gt;; One schema defines: Field types Validation rules Error messages TypeScript types 2. Async Validation Check email availability without blocking the UI: const emailSchema = z.string().email().refine( async (email) =&gt; { const response = await fetch(`/api/check-email?email=${email}`); const { available } = await response.json(); return available; }, { message: \"Email already taken\" } ); With React Hook Form: &lt;input {...register(\"email\", { validate: async (value) =&gt; { const result = await emailSchema.safeParseAsync(value); return result.success || result.error.errors[0].message; }, })} /&gt; 3. Multi-Step Forms Break complex forms into steps: function MultiStepForm() { const [step, setStep] = useState(1); const methods = useForm&lt;UserFormData&gt;(); const onSubmit = async (data: UserFormData) =&gt; { if (step &lt; 3) { setStep(step + 1); return; } // Final submission await createUser(data); }; return ( &lt;FormProvider {...methods}&gt; &lt;form onSubmit={methods.handleSubmit(onSubmit)}&gt; {step === 1 &amp;&amp; &lt;PersonalInfoStep /&gt;} {step === 2 &amp;&amp; &lt;AddressStep /&gt;} {step === 3 &amp;&amp; &lt;ReviewStep /&gt;} &lt;div&gt; {step &gt; 1 &amp;&amp; ( &lt;button type=\"button\" onClick={() =&gt; setStep(step - 1)}&gt; Back &lt;/button&gt; )} &lt;button type=\"submit\"&gt; {step &lt; 3 ? \"Next\" : \"Submit\"} &lt;/button&gt; &lt;/div&gt; &lt;/form&gt; &lt;/FormProvider&gt; ); } 4. Dynamic Field Arrays Add/remove fields dynamically: import { useFieldArray } from \"react-hook-form\"; function SkillsForm() { const { control, register } = useForm(); const { fields, append, remove } = useFieldArray({ control, name: \"skills\", }); return ( &lt;div&gt; {fields.map((field, index) =&gt; ( &lt;div key={field.id}&gt; &lt;input {...register(`skills.${index}.name`)} placeholder=\"Skill name\" /&gt; &lt;input type=\"number\" {...register(`skills.${index}.years`)} placeholder=\"Years\" /&gt; &lt;button type=\"button\" onClick={() =&gt; remove(index)}&gt; Remove &lt;/button&gt; &lt;/div&gt; ))} &lt;button type=\"button\" onClick={() =&gt; append({ name: \"\", years: 0 })} &gt; Add Skill &lt;/button&gt; &lt;/div&gt; ); } 5. Conditional Fields Show/hide fields based on other values: function PaymentForm() { const { register, watch } = useForm(); const paymentMethod = watch(\"paymentMethod\"); return ( &lt;div&gt; &lt;select {...register(\"paymentMethod\")}&gt; &lt;option value=\"card\"&gt;Credit Card&lt;/option&gt; &lt;option value=\"bank\"&gt;Bank Transfer&lt;/option&gt; &lt;/select&gt; {paymentMethod === \"card\" &amp;&amp; ( &lt;&gt; &lt;input {...register(\"cardNumber\")} placeholder=\"Card Number\" /&gt; &lt;input {...register(\"cvv\")} placeholder=\"CVV\" /&gt; &lt;/&gt; )} {paymentMethod === \"bank\" &amp;&amp; ( &lt;&gt; &lt;input {...register(\"accountNumber\")} placeholder=\"Account\" /&gt; &lt;input {...register(\"routingNumber\")} placeholder=\"Routing\" /&gt; &lt;/&gt; )} &lt;/div&gt; ); } Error Handling UX Show errors at the right time: function SmartInput({ name, label, ...props }) { const { register, formState: { errors, touchedFields, isSubmitted }, } = useFormContext(); const error = errors[name]; const showError = error &amp;&amp; (touchedFields[name] || isSubmitted); return ( &lt;div&gt; &lt;label htmlFor={name}&gt;{label}&lt;/label&gt; &lt;input id={name} aria-invalid={showError ? \"true\" : \"false\"} aria-describedby={showError ? `${name}-error` : undefined} {...register(name)} {...props} /&gt; {showError &amp;&amp; ( &lt;span id={`${name}-error`} role=\"alert\"&gt; {error.message} &lt;/span&gt; )} &lt;/div&gt; ); } Only show errors after: User has touched the field, OR Form has been submitted This prevents angry red errors while typing. Performance Optimization For large forms (50+ fields): const { register } = useForm({ mode: \"onBlur\", // Validate on blur, not on change shouldUnregister: true, // Unregister unmounted fields }); Use Controller only for complex inputs: import { Controller } from \"react-hook-form\"; &lt;Controller name=\"birthDate\" control={control} render={({ field }) =&gt; ( &lt;DatePicker value={field.value} onChange={field.onChange} /&gt; )} /&gt; File Uploads Handle files properly: function FileUpload() { const { register, watch } = useForm(); const file = watch(\"avatar\"); return ( &lt;div&gt; &lt;input type=\"file\" accept=\"image/*\" {...register(\"avatar\")} /&gt; {file?.[0] &amp;&amp; ( &lt;img src={URL.createObjectURL(file[0])} alt=\"Preview\" /&gt; )} &lt;/div&gt; ); } Submission with TanStack Query Clean mutation handling: function UserForm() { const methods = useForm&lt;UserFormData&gt;(); const mutation = useMutation({ mutationFn: createUser, onSuccess: () =&gt; { toast.success(\"User created!\"); methods.reset(); }, onError: (error) =&gt; { toast.error(error.message); }, }); const onSubmit = (data: UserFormData) =&gt; { mutation.mutate(data); }; return ( &lt;form onSubmit={methods.handleSubmit(onSubmit)}&gt; {/* fields */} &lt;button type=\"submit\" disabled={mutation.isPending}&gt; {mutation.isPending ? \"Saving...\" : \"Save\"} &lt;/button&gt; &lt;/form&gt; ); } Accessibility Checklist ✅ Labels for every input ✅ Error messages announced to screen readers ✅ Keyboard navigation works ✅ Focus management (auto-focus first error) ✅ Required fields marked ✅ Clear validation feedback Testing Test forms thoroughly: import { render, screen, waitFor } from \"@testing-library/react\"; import userEvent from \"@testing-library/user-event\"; test(\"shows validation errors\", async () =&gt; { render(&lt;UserForm /&gt;); const user = userEvent.setup(); const submitButton = screen.getByRole(\"button\", { name: /submit/i }); await user.click(submitButton); await waitFor(() =&gt; { expect(screen.getByText(/email is required/i)).toBeInTheDocument(); }); }); Conclusion The Motorola contract wizard and the insurance policy builder taught me the same lesson: complex forms aren't a UI problem — they're a data modeling problem. Get the schema right first, handle validation at the schema level, and the rest follows. Schema-first validation. Smart error timing. Performance-aware rendering. Accessibility from day one. Thorough testing. Get these right, and even the most complex forms become manageable. Need help with complex forms? Get in touch.","contentHash":"sha256:084c12ffcc41b1cd7aa832a4606d5ba7b71fd0b1025c17e3e50889489b8d5cea","authorName":"Maksym Kuzmitskyi (MaximusFT)","authorUrl":"https://api.zyvop.com/author/maksym","authorSameAs":["https://ma-x.im/","https://github.com/MaximusFT","https://www.linkedin.com/in/maximusft/"],"category":null,"tags":["Forms","React Hook Form","Architecture"],"audience":"Software engineers and developers building applications with Forms","tone":"Instructional, practical, code-first","readingTimeMinutes":5,"wordCount":1133,"faqs":null,"primaryTopic":"Forms","publishedAt":"2026-07-30T20:08:37.504Z","updatedAt":"2026-08-24T23:05:00.306Z","canonicalUrl":"https://ma-x.im/blog/complex-forms-done-right"}