Skip to main content

Form shell

Use withForm at the root of a form (page, modal, or wizard step) and useForm inside child components.

withForm

withForm wraps your component in FormProvider so descendants can call useForm, useField, and validation hooks.

export const EditProfileModal = withForm(function EditProfileModal(props: ModalProps) {
return (
<Modal open={props.open} onClose={props.onClose}>
<ProfileForm onSave={props.onSave} onClose={props.onClose} />
</Modal>
);
});

useForm

Inside the form tree:

const { getValues, modified, valid, reset } = useForm();
PropertyMeaning
getValues(ref)Current value for a FieldRef or ArrayFieldRef
modifiedUser has changed at least one field since mount/reset
validAll registered validators pass
reset()Reset touched/modified state (after successful save)

Gating submit

Disable primary actions when the form is invalid or unchanged:

<button type="submit" disabled={!modified || !valid}>
Save
</button>

For create-only flows you may omit modified; for edit flows require both so users cannot submit a pristine form.

Serializing for APIs

Keep validation out of serializers. Map getValues to your DTO in a pure helper:

export function getProfileValues(
refs: ProfileFieldRefs,
getValues: FormApi['getValues'],
): ProfilePayload {
return {
name: getValues(refs.name),
tags: getValues(refs.tags).toArray(),
};
}

Throwing in serializers couples UI to API errors — prefer useFieldValidation for user-facing rules.

Server errors

Network or API failures after submit are separate from field validation. Show them in a banner or callout; do not replace field-level errors unless the server returns per-field messages you map onto refs.