Skip to main content

Frontend Forms (React Hook Form)

This page shows how I build forms in the frontend using react-hook-form with MUI components, and how I surface validation and submission errors.

How it works

Here’s the pattern I follow:

1) useForm for data + validation

I start with a typed form model and useForm({ defaultValues }). Example: apps/portal/src/components/memberForm.tsx uses register, handleSubmit, and formState.errors:
Then inputs bind validation rules:

2) Controller when the MUI input needs controlled wiring

When the component isn’t a simple ref-friendly input, I use Controller. Example: apps/portal/src/pages/admin/settings/billing/checkout/newPaymentForm/customerInfo/form.tsx uses Controller with rules={{ required: true }}.

3) Submission uses generated mutation hooks

On submit, the component calls the generated mutation and then renders a user-facing result. Example: billing customer updates read the mutation error and success directly:

4) Keep user-facing messages centralized

For checkout-related UX, I reuse constants from: apps/portal/src/utils/checkout.ts (example: UNEXPECTED_ERROR_MESSAGE).

Extending this system

Here’s how I extend the frontend form pattern when you need a new screen or input set.
  1. Define the form data type and defaultValues.
Example: follow the shape in apps/portal/src/components/memberForm.tsx (MemberForm).
  1. Wire RHF validation rules to each input.
Example: use helperText={errors.field?.message} for MUI TextField.
  1. Use Controller when RHF’s register won’t work cleanly with the MUI component.
  2. Call a generated mutation hook in the submit handler.
  3. Verify:

What not to do

If you skip these, your form UX will stay consistent across the app. ❌ Never bypass generated hooks and call backend endpoints manually from React. ❌ Never duplicate checkout error strings when apps/portal/src/utils/checkout.ts already has them. ❌ Never implement complex business rules (entitlements, scopes, billing math) in the form component.

File conventions

This is where new form code should live, based on how the existing screens are organized. Prefer:
  • apps/portal/src/components/memberForm.tsx style for reusable form components
  • apps/portal/src/pages/.../form.tsx style for screen-specific forms

What’s next?

When your form touches a failure path, the next doc shows how error UX stays user-friendly.