easy-forms
Form components

Repeating groups

Line-item sections with add/remove, bounds, and per-row dependencies.

A repeating group renders a set of fields once per row — invoice lines, contacts, order items. Behaviour lives in two hooks; the markup is the ejectable @ef-pro/repeating-group renderer.

Preview
Live
Pro

The live preview for this Pro component is not configured in this environment. The Code tab shows the full schema, and Easy Forms Pro covers what it renders.

About the badge in the corner

The preview above is unlicensed, so @easy-forms/pro renders its watermark badge. Setting a license key removes it — see Licensing.

useRepeatingGroup

Drives the container: the row list, add/remove, and bounds.

const { indices, add, remove, atMax, canRemove, minItems, maxItems } =
	useRepeatingGroup(props);
PropTypeDefaultDescription
indicesnumber[]Active row indices. Sparse and monotonic — never reindexed.
add() => voidAppend a row. No-op at maxItems.
remove(index: number) => voidRemove a row by index. No-op at minItems.
atMaxbooleanTrue when another row cannot be added.
canRemovebooleanTrue when rows may be removed (count > minItems).
minItemsnumberLower bound from the control config.
maxItemsnumber | undefinedUpper bound from the control config.

Why indices are sparse

Removing row 1 of three leaves [0, 2] — indices are never renumbered. This is deliberate: renumbering would change the field keys of surviving rows, and every field's state (value, touched, dirty, errors) is keyed by name. Sparse indices mean removing a row cannot disturb the rows around it.

useRepeatingGroupItem

Drives one row: key prefixing and that row's own dependency engine.

const { groups } = useRepeatingGroupItem({ groupKey, index, groups, defaultItem });
PropTypeDefaultDescription
groupKey*stringThe container field's key.
index*numberThis row's index, from useRepeatingGroup().indices.
groups*Group[]The per-item group template from the control config.
defaultItemRecord<string, unknown>Seed values for a newly added row.

It returns the template with every key and id prefixed ${groupKey}.${index}., and attaches a dependency engine scoped to that row — the parent form's engine only sees the static schema, not the dynamic item fields, so without this within-row propsDependsOn / valueDependsOn / resetDependsOn would never fire.

The prefixed tree is memoised on the row's stable inputs, so adding or removing other rows never changes this row's field identities.

Reading form-level fields from inside a row

Row fields are namespaced, so a dependency inside a row resolves names within that row by default. Use the $root. prefix to reach a form-level field:

import { ROOT_FIELD_PREFIX } from '@easy-forms/pro';

dependents: {
	propsDependsOn: [
		{
			fieldNames: ['$root.accountType'],
			compute: (v) => ({ hidden: v['$root.accountType'] !== 'business' }),
		},
	],
}

Enforcing bounds at submit

The renderer enforces minItems / maxItems in its UI. To also enforce them at submit time, opt in on the question:

import { repeatingGroupValidators } from '@easy-forms/pro';

validators: repeatingGroupValidators({ minItems: 1, maxItems: 10 });

Getting the renderer

EASY_FORMS_PRO_TOKEN=<your token> npx shadcn@latest add @ef-pro/repeating-group

Requires the free @easy-forms registry to be configured too — see Installing Pro.