Capítulo 20 de 29

Chapter 20: Form Composition

Core Idea

createFormHook lets you pre-bind your own field/form components (a TextField, a SubmitButton, etc.) to a shared context once, so every form in the app gets useAppForm/form.AppField with those components already wired — and withForm/withFieldGroup let you split a large form into sub-components without prop-drilling the form instance through every level.

Key Concepts

  • createFormHookContexts() produces a fieldContext/formContext pair used to build components that can read the current field/form without receiving it as an explicit prop.
  • createFormHook({ fieldContext, formContext, fieldComponents, formComponents }) returns useAppForm (a useForm replacement) and withForm (a HOC for sub-forms), with your custom components (e.g. TextField) pre-registered.
  • useFieldContext<T>() inside a custom component (like TextField) pulls the current field from context — this is what lets TextField be written once and reused across every form built with this hook.
  • withForm wraps a piece of a form (e.g. an AddressSection) so it can be composed into a parent without threading the form instance through props manually — the parent passes form once, sub-components read it via context.
  • withFieldGroup is the analogous tool for reusing a cluster of related fields (not a whole form) across multiple different forms.

Code Examples

const { fieldContext, formContext } = createFormHookContexts()

function TextField({ label }) {
  const field = useFieldContext<string>()
  return (
    <label>
      <span>{label}</span>
      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
    </label>
  )
}

const { useAppForm, withForm } = createFormHook({
  fieldContext, formContext,
  fieldComponents: { TextField },
  formComponents: { SubmitButton },
})

const AddressSection = withForm({
  defaultValues: { street: '', city: '' },
  render: ({ form }) => (
    <form.AppField name="street">{(field) => <field.TextField label="Street" />}</form.AppField>
  ),
})
  • What it demonstrates: the full pipeline from raw contexts to a reusable, pre-styled TextField to a composable AddressSection sub-form — this is the concrete expression of the "built for composition" philosophy from ch003.

Worked Example

A team building a multi-page checkout flow would set up createFormHookContexts/createFormHook once at the app level, registering TextField, SelectField, and SubmitButton as fieldComponents/formComponents. Every page's form then calls useAppForm instead of useForm, immediately getting typed, pre-styled fields via form.AppField — no per-page UI-library wiring (ch018) repeated. A shared AddressSection (built with withForm, as above) can then be dropped into both the "shipping address" and "billing address" steps of the checkout without duplicating its field markup or its validators.

Key Takeaways

  1. Set up createFormHook once per app/design-system, not once per form — this is infrastructure, analogous to radix-primitives-docs/base-ui-docs's "wrap the primitive once" pattern in this same skill library.
  2. withForm solves prop-drilling for splitting one large form into pieces; withFieldGroup solves reusing a field cluster across different forms — pick based on which problem you actually have.
  3. Once createFormHook is set up, prefer useAppForm/form.AppField over raw useForm/form.Field everywhere in the app — reaching for the raw hooks bypasses the whole point of this setup.

Connects To

  • Ch018 UI Libraries Integration: the wiring this chapter's fieldComponents centralizes.
  • Ch003 Philosophy: "built for composition," the design principle this chapter implements.
  • Ch012 Form Groups: a related but distinct multi-step tool — withFieldGroup reuses field definitions, FormGroup scopes validation within one form instance.