Field authoring

A field component in BlueForm is a plain React component with one responsibility: render a piece of UI that reflects and mutates a single form value. It does not manage state, does not know about other fields, and does not handle validation — the engine takes care of all of that before the component renders.

This separation is what makes field components reusable. Write an InputField once, wire it to your design system, and use it across every form in your app without modification.

How a field receives its context

There are two ways a field component receives its context, depending on how it is used:

Via hooks — when the component is registered in fieldMapping and rendered by the engine:

export function InputField() {
  const { value, onChange, label, errorMessage } = useField()
  // ...
}

Via render prop — when the field is defined inline in config, the same props arrive as function arguments — no hook call needed:

name: {
  type: "inline",
  render: ({ value, onChange, label, errorMessage }) => (
    // ...
  ),
}

Both approaches expose the same interface. The render prop is convenient for one-off fields. For fields used across multiple forms, a registered component is the better path.

See useField, useArrayField, and render prop for the full API of each approach.

A minimal field

import { useField } from 'react-headless-form'

export function InputField() {
  const { value, onChange, label, errorMessage, required, disabled } = useField()

  return (
    <div>
      {label && <label>{label}{required && ' *'}</label>}
      <input
        value={value ?? ''}
        onChange={(e) => onChange?.(e.target.value)}
        disabled={disabled}
      />
      {errorMessage && <div>{errorMessage}</div>}
    </div>
  )
}

Notice what this component does not do:

  • It does not import anything from RHF
  • It does not know what form it lives in
  • It does not inspect validation rules
  • It does not handle i18n — label and errorMessage are already translated

Registering field components

Field components are registered once via fieldMapping in setupForm, then referenced by type name in any form config:

const [Form] = setupForm({
  fieldMapping: defineMapping({
    text: InputField,
    select: SelectField,
    datepicker: DatePickerField,
  }),
})
config={{
  username: { type: 'text', label: 'Username' },
  role:     { type: 'select', label: 'Role' },
  birthdate: { type: 'datepicker', label: 'Date of Birth' },
}}

Passing extra props

Each field in config can pass additional props to its component via the props key, typed against the component's own prop interface:

export function InputField(props: React.InputHTMLAttributes<HTMLInputElement>) {
  const { value, onChange } = useField()
  return (
    <input
      {...props}
      value={value ?? ''}
      onChange={(e) => onChange?.(e.target.value)}
    />
  )
}
config={{
  password: {
    type: 'text',
    props: { type: 'password', autoComplete: 'current-password' },
  },
}}

BlueForm enforces at compile time that props matches the component's declared prop types — invalid props are caught before runtime.

Common patterns

Read-only state

A common pattern is to render a plain display value when readOnly is true:

export function InputField() {
  const { value, onChange, readOnly, disabled } = useField()

  if (readOnly) return <span>{value ?? '—'}</span>

  return (
    <input
      value={value ?? ''}
      onChange={(e) => onChange?.(e.target.value)}
      disabled={disabled}
    />
  )
}

Visibility guard

Respect visible at the top of the component:

export function InputField() {
  const { value, onChange, visible } = useField()

  if (!visible) return null

  return (
    <input
      value={value ?? ''}
      onChange={(e) => onChange?.(e.target.value)}
    />
  )
}

Accessibility

Use id and ref from useField for proper label association and RHF's setFocus:

export function InputField() {
  const { id, ref, value, onChange, label, required } = useField()

  return (
    <div>
      <label htmlFor={id}>{label}{required && ' *'}</label>
      <input
        id={id}
        ref={ref}
        value={value ?? ''}
        onChange={(e) => onChange?.(e.target.value)}
      />
    </div>
  )
}