Form

Layout, validation and state for a set of fields, with each field's label and error message linked by its name.

Usage

When to reach for Form, what each of its axes is for, and the decisions it leaves to you.

When to use

Form is a value store with layout attached. It holds every field by name, decides when each one validates, and links each field's label, id and error message. Reach for it when a screen has enough fields that keeping those in step by hand is real work.
  • A record being created or edited, where the fields are submitted together and validated together.
  • Anything with rules: required fields, formats, a field whose validity depends on another field's value.
  • Anything with repeated groups, because Form.List is the part that is hardest to build by hand.
  • Anything where the server can reject a field, because setFields puts that rejection on the field it belongs to.
Use something else when
  • There are two fields and no validation. Plain controlled state is shorter, has no store to reason about, and does not make you learn a name path to read a value back.
  • The form is the whole dialog. A Modal already has a footer with its own confirm button and its own confirmLoading. Put the fields in the body and submit from onOk instead of adding a second submit row. Keep Form if you want the validation, and drop its submit button either way.
  • It is one control that acts immediately. A search box, a filter Select, a Switch that saves on toggle. Nothing is submitted, so there is nothing to collect.
  • The fields are a wizard with its own state machine. One Form per step, held by the step, beats one store spanning steps that mount and unmount under it.

Layout, and where each one stops working

layout takes three values and they are not interchangeable. Each one has a width below which it stops reading as a form.
  • horizontal is the default: label in a column beside the control, aligned down the page. It reads well on a wide settings page and breaks on a narrow one, because the label column and the control column each get half of a space that was already tight. The column widths are labelCol and wrapperCol, which take Grid Col props, so pass a responsive object when the layout only works above md.
  • vertical puts the label above the control and gives the control the full width. It is the safe choice for anything narrow, anything in a drawer, and any form with long labels. It costs vertical space, so a twenty-field form becomes a long scroll.
  • inline flows the items on one line. It is for a filter bar of three or four short fields. It stops working once a field can show an error, because the message appears under one item and shifts the row.
labelAlign (left or right, default right) only means anything under horizontal. labelWrap lets a long label wrap instead of being clipped, and an individual item can override the form with its own layout, which takes horizontal or vertical only.
One stacked item needs full-width columns. layout="vertical" on a single Form.Item stacks its label, but the item still takes the form's labelCol and wrapperCol. Give it { span: 24 } for both so the control spans the form.

Form.Item, and the name that wires it up

name does the wiring. Given one, the Item derives a field id from it (joined with _, prefixed by the form's name when there is one), puts that id on the child control, points the label's htmlFor at it, and keeps the field's value, validation state and error message under the same key.
  • An Item with no name is presentational. It draws a label and takes the layout, and registers no field. Use it for a submit row, to line the button up under the controls.
  • noStyle is the reverse: a registered field with no layout of its own. Its errors show on the nearest styled parent Item, so a control built from two inputs still shows one message.
  • A named Item takes one child element. The Item clones that child to pass it the value, the id and the change handler, so an array of children or a wrapping fragment leaves it nothing to clone. It warns in development and does nothing in production.
  • A nested name is an array: name={['contact', 'email']} reads and writes values.contact.email. The id becomes contact_email, which is the id to use in a test or a hand-written label.
  • Do not put defaultValue on a child inside a named Item. The field is controlled from the store, so the default is ignored with a warning. Use initialValues instead.

Rules, and when they run

rules is an array on the Item. A rule can also be a function of the form instance, which is how one field's rule reads another field's value.
  • validateTrigger decides when rules run. It defaults to onChange, so a required field goes red while the reader is still typing their first character. For anything with a format rule, validateTrigger="onBlur" on the Item is usually kinder, and false turns automatic validation off so only submit checks it.
  • validateDebounce waits for typing to pause. A rule that asks the server whether a username is taken should carry one, in milliseconds, or it asks on every keystroke.
  • Every rule runs by default. Each failure shows its own message. Set validateFirst on the Item to stop at the first failure, in order, and keep a server check from running until the cheap rules pass.
  • A validator fails by rejecting. Return a rejected promise to show an error. A returned string or false shows nothing.
  • dependencies re-validates, shouldUpdate re-renders. A confirm-password field lists the password field in dependencies so it re-checks when that one changes. A field that appears only when another has a particular value uses shouldUpdate with a render function. Setting both on one Item logs a warning.
  • warningOnly does not block submit. A rule marked with it paints the warning color and does not mark the field required.
  • Message wording lives in one place per form: validateMessages takes a template set, so "is required" is written once instead of on forty rules.
  • Write validateMessages as plain strings. Form fills in ${label} and ${min} itself. In a template literal, JavaScript would try to fill them in first.
  • Validation icons come from your icon pack. hasFeedback shows a success, warning, error or loading icon inside the control. An item with no rules shows none until you set its validateStatus. Pass your own feedbackIcons function on the Form, or hasFeedback={{ icons }} on one item, to replace them.

initialValues, and what a reset goes back to

initialValues is read once, when the Form mounts. It is not a controlled prop, so a value that arrives later from a fetch does nothing: the form has already taken its snapshot of {}.
  • For data that loads, either hold the Form back until the data is in hand, or call form.setFieldsValue(data) in an effect when it arrives. Do not do both.
  • form.resetFields() goes back to initialValues, not to empty. On an edit form filled with setFieldsValue, reset clears the record instead of restoring it.
  • Resetting also clears every field's touched and error state. Reset after a successful submit, not after a failed one.
  • An Item can carry its own initialValue, which is what a Form.List row needs, since the row does not exist when the form mounts.
  • preserve={false} on an Item drops its value when the field unmounts. Use it on a field you render conditionally. The default keeps the stale value in the store and submits it.
  • A reused Form that stays mounted between records (a Modal that keeps its subtree alive) needs an explicit reset when the record changes, or clearOnDestroy so the store empties with the form.
  • Form.useWatch returns undefined on the first render. The fields have not registered yet. Fall back to your initial values when you compare against them, or the form reads as changed on mount.

Form.List for repeated groups

Form.List is a field array with a render prop. It draws no element of its own: it hands you the current fields plus add, remove and move, and you decide what a row looks like.
  • Key rows by field.key. Use field.key as the React key, never the index, or removing a row remounts the wrong inputs.
  • A row's field name is an array: name={[field.name, 'quantity']}, relative to the list. Spreading {...field} onto the Item is not enough once the row has more than one input.
  • The list itself can carry rules, which is how "at least one row" is expressed. Its message has no field to attach to, so render it with Form.ErrorList.
  • Do not set preserve={false} on a list field. Removing a row already removes its values, and the flag logs a warning.
  • Unlike Form and Form.Item, Form.List takes no type argument: its values are part of the parent form's type.

Errors that come back from the server

Client rules cannot know that an email is already registered. That answer arrives after submit, and it belongs on the field rather than in a toast that the reader has to hold in their head while they scroll back up.
  • form.setFields([{ name: 'email', errors: ['Already registered'] }]) puts the message under that field and turns it red, with the same markup and the same wiring a client rule would have produced.
  • Set errors: [] on the same field to clear it. The message stays until something removes it.
  • The field's next validation clears errors set this way, so the rejection goes away when the reader edits the email.
  • Keep a failure that belongs to no field (the request timed out, the record was deleted) out of the fields entirely. That is an Alert above the submit row.
  • form.setFields also carries warnings and validating, so a slow server-side check can show the same spinner a rule would.

Controls that do not carry a value

A named Item binds the store to its child's value prop and reads changes back from onChange. A Switch and a Checkbox do not have a value: they have checked. Without being told, the Item writes a boolean into a prop the control ignores, and the control never moves.
  • valuePropName="checked" on the Item is the fix, for Switch, Checkbox and Radio used on its own. A Checkbox.Group and a Radio.Group do use value, so they do not need it.
  • trigger renames the change event for a control that fires something else, and getValueFromEvent maps that event's arguments to the value to store. An Upload is the usual case: it reports a whole event object and you want fileList.
  • getValueProps goes the other way and shapes what the store hands the control. It is how an ISO date string in the store reaches a DatePicker that wants a date object.
  • normalize runs on the way in, so trimming a string or stripping a mask happens once, in the store, rather than in every reader of the value.

Submitting, and what to disable

onFinish fires with the values once every rule has passed. onFinishFailed fires instead, with the fields that failed. Handle both, so a failed submit tells the reader what to fix.
  • Do not disable submit until the form is valid. A button that is dead for reasons the reader cannot see is worse than one that fails and says why. Let the click happen and let onFinishFailed do the explaining. If you do disable it, check with validateFields({ validateOnly: true }), which validates without showing errors.
  • Do disable it while the submit is in flight, or put the button in loading, which disables it and shows why. Two clicks on a create form make two records.
  • disabled on the Form disables every control inside it, which suits a form that is saving. size and variant reach every control the same way, so set them on the Form instead of on each field.
  • scrollToFirstError takes an options object ({ behavior, block, focus }), which is how you keep a sticky header from covering the field it just scrolled to.
  • Submit with htmlType="submit" on a button inside the form, not an onClick that calls form.submit(). A native submit button also submits on Enter.
  • After a successful create, reset. After a successful edit, do not: the values on screen are now the saved ones, and resetting throws them back to the snapshot taken at mount.