React Form with Dynamic Fields
Dynamic fields let users add as many entries as they need — phone numbers, addresses, team members — without the form designer needing to predict the count in advance. The state is an array of strings, and each field maps to one entry. Keys must be stable identifiers (not array indices) to prevent React from losing focus when items are removed.
The challenge
Adding and removing fields dynamically requires careful state management, stable key assignment, and handling edge cases like minimum field counts.
- Using array indices as keys causes inputs to lose focus when an item is removed from the middle
- Preventing the user from removing the last remaining field
- Enforcing a maximum field count with a clear disabled state on the add button
- Updating a single item in the array without mutating state
Working code example
Here is a complete, self-contained working example you can drop directly into any React project. It uses Tailwind CSS for styling and requires no external dependencies beyond React itself.
import { useState } from 'react';
const MAX_PHONES = 5;
let nextId = 1;
export default function DynamicPhoneFields() {
const [phones, setPhones] = useState([{ id: nextId++, value: '' }]);
const [submitted, setSubmitted] = useState(false);
const add = () => {
if (phones.length >= MAX_PHONES) return;
setPhones((prev) => [...prev, { id: nextId++, value: '' }]);
};
const remove = (id) => {
if (phones.length <= 1) return;
setPhones((prev) => prev.filter((p) => p.id !== id));
};
const update = (id, value) => {
setPhones((prev) => prev.map((p) => (p.id === id ? { ...p, value } : p)));
};
const handleSubmit = (e) => {
e.preventDefault();
const filled = phones.filter((p) => p.value.trim());
if (!filled.length) { alert('Enter at least one phone number'); return; }
setSubmitted(true);
};
if (submitted)
return (
<div className="max-w-sm mx-auto p-6 bg-green-50 rounded-2xl text-green-700">
<p className="font-semibold">Phone numbers saved!</p>
<ul className="mt-2 space-y-1 text-sm">
{phones.filter((p) => p.value.trim()).map((p) => (
<li key={p.id}>{p.value}</li>
))}
</ul>
</div>
);
return (
<form onSubmit={handleSubmit} className="max-w-sm mx-auto p-6 bg-white rounded-2xl shadow space-y-4">
<h2 className="text-xl font-bold text-gray-800">Phone Numbers</h2>
<div className="space-y-2">
{phones.map((phone, index) => (
<div key={phone.id} className="flex items-center gap-2">
<input
type="tel"
value={phone.value}
onChange={(e) => update(phone.id, e.target.value)}
placeholder={`Phone ${index + 1}`}
className="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500"
/>
<button type="button" onClick={() => remove(phone.id)} disabled={phones.length === 1}
className="w-8 h-8 flex items-center justify-center rounded-lg border border-gray-300 text-gray-400 hover:text-red-500 hover:border-red-300 disabled:opacity-30 disabled:cursor-not-allowed transition">
×
</button>
</div>
))}
</div>
<button type="button" onClick={add} disabled={phones.length >= MAX_PHONES}
className="w-full border border-teal-500 text-teal-600 text-sm font-medium py-2 rounded-lg hover:bg-teal-50 disabled:opacity-50 disabled:cursor-not-allowed transition">
+ Add phone number {phones.length >= MAX_PHONES ? '(max reached)' : `(${phones.length}/${MAX_PHONES})`}
</button>
<button type="submit"
className="w-full bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition">
Save Numbers
</button>
</form>
);
}How ReactForm.co helps
ReactForm's visual builder handles all of the above — dynamic fields configuration, validation rules, state management, and responsive layout — without writing a single line of code. Drag fields onto the canvas, configure their properties in the sidebar, and get production-ready React output. You can publish the form and collect responses instantly, or export the JSX to drop into your own codebase.
Build this form visuallyFrequently asked questions
Why should I not use array index as the key for dynamic list items?
When you remove an item from the middle of the list, all subsequent items get new indices. React uses the key to match DOM nodes to state, so reindexed items get matched to the wrong DOM node, causing inputs to lose their values or focus. Use a stable unique id — a counter, a uuid, or a timestamp — as the key.
How do I update one item in an array without mutating state?
Use Array.map to create a new array: setItems(prev => prev.map(item => item.id === targetId ? { ...item, value: newValue } : item)). This creates a new array and a new object for the updated item without modifying the original state. Never push to or splice the existing state array directly.
How do I remove one item from an array in React state?
Use Array.filter: setItems(prev => prev.filter(item => item.id !== idToRemove)). This returns a new array without the removed item. Combined with a stable id key, this correctly removes the intended item and React efficiently reconciles the remaining DOM nodes.
Related topics
Build this form visually — no code needed
ReactForm.co handles dynamic fields fields, validation, conditional logic, and responsive layout automatically. Publish in minutes and collect responses for free.

