Skip to main content
Field arrays let users work with dynamic lists of structured data — invoice line items, contact entries, shipping addresses, checklist items. In FormEngine, the Repeater component handles this: it renders a template of fields for each item in an array and lets users add, remove, and reorder rows.

How it works

A Repeater is a component that takes an array in form data and renders its child components once per array item. When a user clicks “Add”, a new empty row appears. When they click “Remove”, that row’s data is deleted from the array.
{
  "key": "contacts",
  "type": "Repeater",
  "children": [
    {
      "key": "name",
      "type": "RsInput",
      "props": { "label": { "value": "Name" } }
    },
    {
      "key": "email",
      "type": "RsInput",
      "props": { "label": { "value": "Email" } },
      "schema": {
        "validations": [{ "key": "email" }]
      }
    }
  ]
}
Form data output:
{
  "contacts": [
    { "name": "Alice", "email": "alice@example.com" },
    { "name": "Bob", "email": "bob@example.com" }
  ]
}

Validation per row

Each row validates independently. If row 2 has an invalid email, only row 2 shows the error — the rest of the form remains clean. Validation rules defined inside a Repeater’s children apply per-item. You can also add a min / max items rule on the Repeater itself to enforce “at least 1 contact” or “maximum 10 line items.”

Conditional logic inside rows

Conditional rendering works within each row. For example, show a “Company” field only when the “Type” dropdown is set to “Business”:
{
  "key": "type",
  "type": "RsDropdown",
  "props": { "data": { "value": [{"label": "Personal", "value": "personal"}, {"label": "Business", "value": "business"}] } }
},
{
  "key": "company",
  "type": "RsInput",
  "props": { "label": { "value": "Company" } },
  "conditions": {
    "visible": { "field": "type", "value": "business" }
  }
}
Each row evaluates conditions against its own data — changing row 1’s type doesn’t affect row 2.

Pre-populating rows

Pass initial array data via initialData:
<FormViewer
  view={view}
  getForm={getForm}
  initialData={{
    contacts: [
      { name: "Alice", email: "alice@example.com" },
      { name: "", email: "" }
    ]
  }}
/>

Reading array data

Array data flows through onFormDataChange like any other field:
const onFormDataChanged = (formData: IFormData) => {
  const contacts = formData.data.contacts as Array<{ name: string; email: string }>
  console.log(`${contacts.length} contacts`)
}
Or read imperatively via viewerRef:
const contacts = viewerRef.current?.formData.data.contacts

Styling rows

Each row renders inside a container you can style. Target rows via CSS:
.repeater-row {
  border: 1px solid #e5e7eb;
  padding: 16px;
  margin-bottom: 8px;
  border-radius: 8px;
}

Common patterns

Invoice line items — product name, quantity, unit price, computed total per row, grand total outside the repeater via computed properties. Multi-address forms — primary/secondary address with “same as primary” checkbox that copies data between rows. Checklist / inspection — dynamic check items with pass/fail/N/A per row, conditional follow-up fields, signature at the bottom.

Performance considerations

Repeaters with many rows (50+) can affect render performance. Tips:
  • Enable virtualization if available in your UI pack
  • Minimize computed properties inside rows
  • Avoid deep nesting (repeater inside repeater) unless necessary
  • Use React.memo wrappers for custom components inside rows
Last modified on April 22, 2026