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 and none is pending
pendingAn async validator is still running
hasErrorsSome registered validator has an error, regardless of pending
invalidFieldsList of the refs that have an error
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.