Dynamic form fields are inputs that change based on user interaction or data — for example:
- Adding or removing input fields on the fly.
- Rendering fields conditionally based on a selection.
- Populating form inputs from API data.
- Handling arrays of form inputs (like multiple phone numbers, skills, or addresses).
Common Use Cases:
- “Add More” buttons (e.g., adding more email addresses).
- Conditional rendering (e.g., only show "Company Name" if "Employed" is selected).
- Dynamic from API (form structure is fetched from backend and rendered).
- Step-by-step forms where fields change per step.
import React, { useState } from 'react';
export default function DynamicFieldsExample() {
const [fields, setFields] = useState(['']);
const handleChange = (index: number, value: string) => {
const updated = [...fields];
updated[index] = value;
setFields(updated);
};
const addField = () => setFields([...fields, '']);
const removeField = (index: number) =>
setFields(fields.filter((_, i) => i !== index));
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(fields);
};
return (
<form onSubmit={handleSubmit}>
{fields.map((value, index) => (
<div key={index}>
<input
value={value}
onChange={(e) => handleChange(index, e.target.value)}
placeholder={`Field ${index + 1}`}
/>
<button type="button" onClick={() => removeField(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={addField}>
Add More
</button>
<button type="submit">Submit</button>
</form>
);
}How it works:
- State is an array of field values.
- Adding/removing updates the array.
- Rendering is based on
.map().
React Hook Form has useFieldArray for this exact scenario.
import React from 'react';
import { useForm, useFieldArray } from 'react-hook-form';
type FormData = {
emails: { value: string }[],
};
export default function RHFDynamicFields() {
const { register, control, handleSubmit } =
useForm <
FormData >
{
defaultValues: { emails: [{ value: '' }] },
};
const { fields, append, remove } = useFieldArray({
control,
name: 'emails',
});
const onSubmit = (data: FormData) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`emails.${index}.value`)} placeholder="Email" />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ value: '' })}>
Add Email
</button>
<button type="submit">Submit</button>
</form>
);
}Advantages:
- Automatically handles field registration.
- Maintains form state even after removing fields.
- Works with validation (Zod, Yup).
Sometimes the backend sends the form structure as JSON, and you loop through it to render fields.
import React, { useState, useEffect } from "react";
type Field = { name: string; type: string; label: string };
export default function APIDynamicForm() {
const [fields, setFields] = useState<Field[]>([]);
useEffect(() => {
// Simulating API call
setTimeout(() => {
setFields([
{ name: "name", type: "text", label: "Name" },
{ name: "age", type: "number", label: "Age" },
]);
}, 500);
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Collect data manually or use form libraries
};
return (
<form onSubmit={handleSubmit}>
{fields.map((f) => (
<div key={f.name}>
<label>{f.label}</label>
<input type={f.type} name={f.name} />
</div>
))}
<button type="submit">Submit</button>
</form>
);
}This is useful when your form structure is not fixed.
- State shape matters → For multiple fields, store data as arrays/objects.
- Unique keys → Use a stable id (like uuid) instead of array index if you expect reordering.
- Validation → Works with Zod/Yup + useFieldArray.
- Performance → Avoid unnecessary re-renders by splitting components.