Form with Dynamic Fields Development
Dynamic fields are groups that user can add and remove while filling form. Typical scenarios: passenger list, order items, document co-authors.
Implementation via React Hook Form useFieldArray
import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const passengerSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
birthDate: z.string(),
passport: z.string().regex(/^\d{4}\s\d{6}$/),
});
const formSchema = z.object({
passengers: z.array(passengerSchema).min(1).max(9),
});
type FormValues = z.infer<typeof formSchema>;
export function PassengersForm() {
const { control, register, handleSubmit, formState: { errors } } = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: { passengers: [{}] },
});
const { fields, append, remove, move } = useFieldArray({
control,
name: 'passengers',
});
return (
<form onSubmit={handleSubmit(console.log)}>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="border rounded-xl p-4 relative">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium">Passenger {index + 1}</h3>
{fields.length > 1 && (
<button
type="button"
onClick={() => remove(index)}
className="text-red-500 text-sm hover:text-red-700"
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm mb-1">First Name</label>
<input
{...register(`passengers.${index}.firstName`)}
className="input-field"
/>
{errors.passengers?.[index]?.firstName && (
<p className="text-red-500 text-xs mt-1">
{errors.passengers[index].firstName?.message}
</p>
)}
</div>
<div>
<label className="block text-sm mb-1">Last Name</label>
<input {...register(`passengers.${index}.lastName`)} className="input-field" />
</div>
<div>
<label className="block text-sm mb-1">Passport</label>
<input
{...register(`passengers.${index}.passport`)}
placeholder="1234 567890"
className="input-field"
/>
</div>
</div>
</div>
))}
</div>
{fields.length < 9 && (
<button
type="button"
onClick={() => append({})}
className="btn-secondary mt-4 w-full"
>
+ Add Passenger
</button>
)}
<button type="submit" className="btn-primary mt-4 w-full">
Continue
</button>
</form>
);
}
Drag-and-Drop Sorting
For forms where element order matters (order items, task priorities):
import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
function SortableFieldItem({ field, index, onRemove }: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: field.id });
return (
<div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}>
<div {...attributes} {...listeners} className="cursor-grab p-1">⠿</div>
{/* field items */}
</div>
);
}
// In main component:
function handleDragEnd(event) {
const { active, over } = event;
if (active.id !== over.id) {
const from = fields.findIndex(f => f.id === active.id);
const to = fields.findIndex(f => f.id === over.id);
move(from, to);
}
}
Timeframe
Form with dynamic fields, add/remove and validation: 2–4 working days.







