Forms pattern
Forms use React Hook Form with custom controlled components as described in the shadcn React Hook Form guide: full control over markup and styling via <Controller> and shadcn <Field /> primitives.
Conventions
- Schema: Define form shape with a Zod schema; use
z.infer<typeof schema>foruseForm<T>. - Setup:
useFormwithresolver: zodResolver(schema)anddefaultValues. - Fields: Use React Hook Form's
<Controller>withcontroland a render prop that receives{ field, fieldState }. Wrap each control in shadcn<Field data-invalid={fieldState.invalid}>, use<FieldLabel>, put the input/select/textarea inside and spreadfield(or usefield.value/field.onChangefor non-inputs). Setaria-invalid={fieldState.invalid}on the control; show errors with<FieldError errors={[fieldState.error]} />whenfieldState.invalid. - Form context: Wrap the form content in
<FormProvider {...form}>so field components can obtaincontrolviauseFormContext()and do not need to receivecontrolas a prop. - ControlledField: A shared primitive under
src/components/forms/fieldsthat wrapsControllerwith the Field + FieldError pattern. It acceptsname, optionalorientationandclassName, and achildrenrender prop(field, fieldState) => ReactNode. Control is always taken fromuseFormContext(); use only inside<FormProvider>. Use it when building reusable field components so they don't duplicate Controller + Field + FieldError. - Reusable field components: Build on ControlledField (or Controller + Field directly) under
src/components/forms/fields. InputField (optionallabel,labelAddon), NumberField (optionallabel,labelAddon;type="number"withvalueAsNumberso form state is numeric; empty input →undefined), CheckboxField (label, optional description), Button (generic:type?: 'submit' | 'button' | 'reset', default'submit'; wraps Field + shadcn Button), and SubmitButton (abstraction over Button withtype="submit"). Field components do not takecontrol; they must be used inside FormProvider. - Submit: Use a TanStack Query mutation whose
mutationFnis a frontend service function (e.g.authService.login). In the submit handler (form.handleSubmit(onSubmit)): callmutation.mutateAsync(data); on returned result, ifstatusCodeis 2xx handle success (redirect, invalidate); if 4xx, set the responsemessageas a root error (form.setError('root', { type: 'server', message })) so it can be shown as a form-level alert, and iffieldErrorsare present apply them withform.setError(field, { type: 'server', message }). Server errors (5xx) throw from the service layer; handle them in the mutation’sonError(e.g. show a toaster). Disable the submit button whenmutation.isPending. Seeresponses. - Generic Form component: A
Formcomponent insrc/components/formsabstractsuseForm,useMutation, and submit/error handling. It accepts type parametersTInput,TData(form values andmutationFninput), andTOutput(success response data). Props:schema,mutationFn(data: TData)(returns ClientApiResponse<TOutput>; throws on 5xx),defaultValues, optionalonSuccess(result: SuccessResponse<TOutput>)(called only whenresult.type === 'success';result.datais typed), optionalonError(response: ServerErrorResponse)(called when the mutation throws; the Form maps ServerError to ServerErrorResponse inside useMutation so both the default handler and customonErrorreceive ServerErrorResponse), and a children render prop that receives{ form, mutation, onSubmit }. On client-error (4xx) the Form sets the response message as a root error and renders a form-level alert; it also appliesfieldErrors. Use it to avoid repeating the same wiring in every form. - Validation modes:
mode: 'onChange' | 'onBlur' | 'onSubmit' | 'onTouched' | 'all'(defaultonSubmit).
Implementation
The boilerplate realizes this pattern by wiring shadcn and react-hook-form in the desktop app (see stack form-handling and the boilerplate-regenerate skill): dependencies installed, shadcn Field components available, a generic Form component in src/components/forms (abstracts useForm, useMutation, onSubmit, and error handling; renders a form-level alert for 4xx root errors and applies fieldErrors; children receive form, mutation, onSubmit), ControlledField and reusable field components in src/components/forms/fields (InputField, NumberField, CheckboxField, Button, SubmitButton) when used, and at least one form (e.g. login) built with Form or FormProvider and zodResolver.
References
- Stack:
form-handling,validation,responses,requests,ui-components. - Canonical reference: shadcn React Hook Form — the doc remains the canonical reference for markup and examples.