Skip to content

Dynamic Fields

Rapid Form automatically tracks fields that are added or removed after the initial render. Whether you are showing a field conditionally or managing a list of rows, errors and values for removed fields are cleaned up without any additional code.


Use a boolean state value to mount or unmount a field. When the field disappears from the DOM, Rapid Form removes its entry from errors state automatically.

import { useState } from 'react';
import { useRapidForm } from 'rapid-form';
export function ConditionalFieldForm() {
const { refValidation, errors } = useRapidForm();
const [showPhone, setShowPhone] = useState(false);
return (
<form ref={(ref) => refValidation(ref, { validations: {
email: { required: true, type: 'email' },
phone: { required: true },
}})}>
<label>
<input type="email" name="email" placeholder="Email" />
{errors['email']?.isInvalid && <span>{errors['email'].message}</span>}
</label>
<label>
<input
type="checkbox"
checked={showPhone}
onChange={(e) => setShowPhone(e.target.checked)}
/>
{' '}Add phone number
</label>
{showPhone && (
<label>
<input type="tel" name="phone" placeholder="Phone" />
{errors['phone']?.isInvalid && <span>{errors['phone'].message}</span>}
</label>
)}
<button type="submit">Submit</button>
</form>
);
}

When the checkbox is unchecked, the phone field unmounts. Rapid Form detects the removal and clears the phone error from state — the error span disappears along with the field.


A field array is a dynamic list of rows where each row contains one or more inputs. Use an array in state to drive the list, and give each input a unique name (e.g. email.0, email.1). When a row is removed, Rapid Form clears the corresponding error from state.

import { useState } from 'react';
import { useRapidForm } from 'rapid-form';
type Row = { id: number };
let nextId = 2;
export function EmailListForm() {
const { refValidation, errors } = useRapidForm();
const [rows, setRows] = useState<Row[]>([{ id: 1 }]);
const addRow = () => setRows((prev) => [...prev, { id: nextId++ }]);
const removeRow = (id: number) =>
setRows((prev) => prev.filter((r) => r.id !== id));
return (
<form ref={(ref) => refValidation(ref, { validations: Object.fromEntries(
rows.map((_, i) => [`email.${i}`, { required: true, type: 'email' as const }])
)})}>
{rows.map((row, i) => (
<div key={row.id}>
<input
type="email"
name={`email.${i}`}
placeholder={`Email ${i + 1}`}
/>
{errors[`email.${i}`]?.isInvalid && (
<span>{errors[`email.${i}`].message}</span>
)}
{rows.length > 1 && (
<button type="button" onClick={() => removeRow(row.id)}>
Remove
</button>
)}
</div>
))}
<button type="button" onClick={addRow}>
Add row
</button>
<button type="submit">Submit</button>
</form>
);
}

When a row is removed, the input for that index unmounts. Rapid Form detects the removal and removes the matching error from state. The error span for that row disappears along with the row itself.


Rapid Form relies on an inline ref callback pattern:

<form ref={(ref) => refValidation(ref, options)}>

When a field is removed from the DOM, rapid-form’s ref callback synchronously compares the current field list against its registry and dispatches a cleanup action for any missing fields.

A MutationObserver is also attached to the form element as a safety net — it picks up fields added or removed by non-React code (e.g. direct DOM manipulation or third-party libraries) outside of the React render cycle.


Field names that clash with JavaScript built-in object properties are silently ignored for security reasons. Avoid using the following names:

  • __proto__
  • constructor
  • prototype

All other field names work as expected.