<Form />

Form is the first item in the array returned by setupForm.

const [ 
  Form, 
  defineConfig,
  Section,
] = setupForm(); 
Tip

Form is used throughout this documentation, but since setupForm() returns an array, you can name it anything.

Form bound to field mapping

When a Form is created, it is bound to a specific field mapping. This mapping defines which field types are available and how they are rendered.

By default, BlueForm provides a set of built-in structural field types (inline, section, array, hidden). Calling setupForm() with no arguments creates a form system that only includes these built-in types.

const [Form] = setupForm();

To extend the form with custom fields while keeping all built-in ones available, use defineMapping:

const [Form] = setupForm({
  fieldMapping: defineMapping({
    text: InputField,
    select: SelectField,
  }),
});

Any unknown field type will be caught at compile time.

config={{
  username: { type: "text",    },  // ✅ custom
  profile:  { type: "section", },  // ✅ built-in
  age:      { type: "number",  },  // ❌ compile-time error
}}

Form bound to a data model

Form is generic. Field keys must match the data model, and onSubmit always receives fully typed data.

<Form<LoginData>
  config={{
    username: { type: "text" },
    password: { type: "text" },
  }}
  onSubmit={(data) => {
    // data is inferred as LoginData
    data.username
    data.password
  }}
/>

Form props

config

The field configuration object that defines the structure of the form. Each key maps to one field entry, including its type, label, rules, render behavior, and any extra props.

See Field config below.

onSubmit

Called when the form is submitted and passes validation. Receives the typed form data, the full React Hook Form methods, and the optional submit event:

onSubmit={(data, formMethods, event) => {
  console.log(data)
}}

Use this for submit-time effects such as API calls, persistence, navigation, or analytics. If you need to derive field values while the user is typing, use onFieldChange instead.

onSubmitSuccess

Called after onSubmit resolves successfully. Receives the same arguments as onSubmit.

This is a good place for post-submit side effects such as reset(), toast notifications, or closing a dialog after a successful save.

onSubmitError

Called when submission fails due to validation errors. Receives the RHF errors object.

Use this when you need to react to invalid submission attempts, for example by scrolling to the first error or showing a summary banner.

defaultValues

Initial values for the form. These are passed into RHF useForm and always take precedence over formOptions.defaultValues.

<Form defaultValues={{ username: "alice" }} ... />

Use this for initial edit-state data, server-provided defaults, or sensible empty-state values.

formOptions

Options passed to RHF useForm for this specific form instance. These merge over formOptions defined in setupForm and per-form values always win.

<Form formOptions={{ resolver: zodResolver(schema) }} ... />

Common use cases:

  • resolver for schema validation
  • mode to override the default validation trigger
  • shouldUnregister to control hidden or unmounted field retention

Use formOptions when you want BlueForm to keep handling rendering and field wiring, but still need lower-level RHF behavior control.

readOnly

When true, all fields in the form receive readOnly: true.

This is a form-level hint for your field components. BlueForm passes the flag down, but each renderer is still responsible for deciding how read-only mode should look and behave.

onFormChange

Called whenever any field value changes. Receives the current full form values and the full RHF form methods.

This is most useful for autosave, external state sync, analytics, or debugging. Because it fires on every change, consider combining it with debounceMs for anything non-trivial.

onFieldChange

Called when a single field value changes. Receives a typed context object plus the full RHF form methods:

onFieldChange={({ name, value, values, setValue, trigger, formState }, form) => {
  if (name === "title") {
    setValue("slug", value?.toLowerCase().replace(/\s+/g, "-") ?? "")
  }
}}

The first argument contains:

FieldDescription
nameThe path of the field that just changed
valueThe new value of that field
valuesCurrent snapshot of the full form values
setValueRHF setValue helper for writing derived values
triggerRHF trigger helper for revalidation flows
formStateCurrent RHF formState snapshot

Derived fields and side effects like title -> slug can be handled with this API, but use it carefully once many fields start depending on each other - update loops can shoot you in the foot.

debounceMs

Delay in milliseconds before onFormChange and onFieldChange fire. Useful for debouncing side effects such as autosave, remote lookups, or expensive synchronization work. Set to 0 or omit to disable.

This affects the change hooks only. It does not change how RHF validation itself works.

renderRoot

Controls the outer form shell, usually the <form> element and any wrappers around it.

Can be provided per-form to override the value set in setupForm.

Important

If neither setupForm nor the <Form /> instance provides renderRoot, an error will be thrown at run time.

i18nConfig

Controls how labels, descriptions, and validation messages are translated.

Can be provided per-form to override the value set in setupForm.

Info

The only option that cannot be overridden is fieldMapping - it is fixed at setup time to preserve compile-time type safety.

Form accepts children

children accepts either a ReactNode or a render function that receives form methods - useful when rendering dynamic UI that depends on form state. It is rendered after the config-driven content, so it usually appears at the end of the form. Both the generated fields and this children content are passed together as the children of renderRoot.

// ReactNode - static
<Form config={config}>
  <button type="submit">Submit</button>
</Form>

// Render function - reactive, accesses form state
<Form config={config}>
  {({ formState, setValue }) => (
    <button type="submit" disabled={!formState.isDirty}>
      Submit
    </button>
  )}
</Form>

The render function receives the same curated set of form methods as renderRoot - see renderRoot for the full list.

Field config

Each entry in config describes a single field. All fields share a common set of options:

OptionTypeDescription
typestringField type - must match a key in fieldMapping or a built-in type
labelstring | TranslatableTextField label, passed through i18n pipeline
descriptionstring | TranslatableTextHelper text, passed through i18n pipeline
defaultValueanyInitial value for the field
rulesValidationRulesRHF validation rules - disabled when formOptions.resolver is provided
propsComponentProps<...>Extra props passed to the field component - typed against the component's prop interface
renderRenderFnPer-field render override - see render prop
visibleboolean | (values) => booleanControls field visibility - hidden fields unmount, so state retention depends on whether the field ever mounted and on RHF shouldUnregister
disabledboolean | (values) => booleanControls disabled state - disabled fields are excluded from the submit payload
readOnlybooleanField-level read-only state - merged with form-level readOnly

visible and disabled can also be functions. In that case they receive the current form values and are evaluated reactively on every change:

email: {
  type: "text",
  visible: (values) => values.contactMethod === "email",
},
confirm: {
  type: "text",
  disabled: (values) => !values.password,
},

For the full runtime behavior - including hidden-from-start fields, defaultValue, and shouldUnregister - see conditional rendering.

rules uses standard React Hook Form validation rules. These are automatically disabled when formOptions.resolver is provided, so schema-driven forms should keep validation in the resolver instead.

username: {
  type: "text",
  rules: {
    required: "Username is required",
    minLength: { value: 3, message: "At least 3 characters" },
  },
}