react-data-form

Anatomy of a form

useForm, FormElement, and the round trip a value makes between them.

Two halves#

A form is always the same two calls. useForm takes the description and gives back a form context — the built form, its current data, and the handlers. FormElement takes that context and renders it.

const formContext = useForm({
  form,        // the description
  data,        // initial values, as your API hands them over
  onChange,    // fires on every keystroke
  onSubmit,    // fires once the validators pass
  asyncData,   // a promise resolving to the initial values
})

Keeping them apart is what lets a form be rendered somewhere other than where it is built: a filter bar in a toolbar, a sub-form inside a dialog, a wizard whose navigation lives in a footer.

What useForm returns#

NameTypeDefaultDescription
formFormBuiltInterfaceThe built form: every field normalised, given a name, and carrying its current value and violations.
onChange(input: FormInputInterface) => voidWhat a controller calls. It updates one field and rebuilds the form around it.
onSubmit() => Promise<Data | undefined>Validates, and resolves with the payload — or with undefined when a validator threw.
updateData(data: Data, partial?: boolean) => voidWrites values in from the outside. Partial by default, so a subset merges.
updateForm(form: FormBuiltInterface) => FormBuiltInterfaceReplaces the description itself — how steps move, and how a field can add another.
readybooleanFalse until asyncData (or form.getData) has resolved. True immediately when there is neither.
isLoadingBooleanStateInterfaceTrue while a submit is in flight; the submit button reads it.
onSuccessPubSub<Data>Publishes after every successful submit — how a dialog knows to close.

The round trip of a value#

Nothing in the library reads a DOM event. A controller is handed the field and a callback, and the loop is closed by the context:

  • FormInputs walks form.inputs and renders each field through its controller.
  • The controller calls onChange({ ...formInput, value }) — the whole field, not just the value.
  • useForm merges it back into the form and re-renders. A field that is readonly is ignored here, which is where read-only is actually enforced.
  • On submit, every validator runs, violations are attached to their fields, and onSubmit only fires if none was raised.

Why the whole field, not just the value

A controller sometimes has more to say than a value — a set of options it has just fetched, a violation it raised itself, a nested form it built. Passing the field back whole lets it change any of that in the same call.

FormElement is four components#

FormElement is a convenience, and a very thin one. It is exactly this:

import {
  FormProvider,
  FormDecorator,
  FormHeader,
  FormInputs,
  FormErrors,
  FormSubmitAction,
} from "react-data-form"

// This *is* FormElement — nothing more.
<FormProvider formContext={formContext}>
  <FormDecorator>
    <FormHeader />
    <FormInputs />
    <FormErrors />
    <FormSubmitAction />
  </FormDecorator>
</FormProvider>

Which means you can assemble those pieces yourself when the layout calls for it — or swap a single one through the form's components key and keep the rest:

useForm({
  form: {
    inputs: { email: { label: "Email", type: "email" } },
    components: {
      // Replace one piece, keep the rest.
      formSubmitAction: MyOwnSubmitBar,
    },
  },
})
NameTypeDefaultDescription
components.formSubmitActionFCThe submit bar. Replaced by the step navigation in a wizard.
components.formInputsFCHow fields are laid out. createGroupForm swaps this one to get sections.
components.formDecoratorFC<FormDecoratorPropsInterface>The wrapping element — the <form> tag and its layout.
components.formErrorsFCForm-level errors, as opposed to per-field violations.
components.formGroupProviderFC<FormGroupProviderPropsInterface>The wrapper around each field — its label, description and violation.

Loading initial values#

A create form starts empty; an edit form starts with what the API has. Pass data when you already hold it, and asyncData when you do not:

const formContext = useForm({
  form: { inputs: { name: { label: "Name" } } },
  // The form renders straight away; `ready` flips once the promise settles.
  asyncData: () => api.get("/me").then((response) => response.data),
})

if (!formContext.ready) return <Loader />

ready is not isLoading

ready is about the initial values and flips once. isLoading is about a submit in flight and flips on every submission. Rendering a spinner on the wrong one gives a form that disappears every time it is saved.

Saving on change#

Some forms have no submit button — a settings panel, a filter bar. Set saveOnChange and the form submits itself, debounced, 1.5 seconds after the last edit.

{
  saveOnChange: true, // submits 1.5s after the last keystroke
  onSubmit: (data) => api.patch("/settings", data),
}

saveOnChange — edit a field and stop typing

This is exactly how the filter bar of react-resource-view works: it is an ordinary form with saveOnChange, whose onChange rewrites the query string.

Next

The next page goes through every key a field description accepts, and what each one changes on screen.