Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rijvi-mahmud/shaddy/llms.txt

Use this file to discover all available pages before exploring further.

Many real-world forms require the user to enter a variable number of items — a list of phone numbers, a set of product variants, a collection of addresses. Managing a dynamic list with react-hook-form’s useFieldArray requires wiring up the hook, passing control, and tracking the array state yourself. FieldArray wraps all of that into a single declarative component.

How It Works

FieldArray uses the function-as-children (render props) pattern. It calls useFieldArray internally with the correct control from useFormContext, then passes the full UseFieldArrayReturn object to your render function. You get fields, append, remove, prepend, update, and every other method from useFieldArray without writing any hook calls yourself.
// Internally, FieldArray does this for you:
const { control } = useFormContext<T>();
const fieldArray = useFieldArray({ control, name });
return children(fieldArray);

Installation

1

Install via CLI

The quickest way to add FieldArray to your project:
npx shadcn@latest add https://shaddy-docs.vercel.app/r/field-array
2

Or install manually

First ensure ShaddyForm is installed:
npx shadcn@latest add https://shaddy-docs.vercel.app/r/shaddy-form
Then copy the component source into your project and update import paths.

Props

name
ArrayPath<T>
required
The dot-notation path to the array field in your schema. Must match an array key in your Zod schema (e.g., "items", "phoneNumbers"). The generic type T is inferred from the surrounding ShaddyForm.
children
(fieldArray: UseFieldArrayReturn<T, ArrayPath<T>>) => ReactNode
required
A render function that receives the full useFieldArray return value. Use the destructured fields, append, and remove (among others) to build your dynamic list UI.

Usage

The children function receives a UseFieldArrayReturn object. The most commonly used properties are:
PropertyTypeDescription
fieldsFieldArrayWithId[]The current list of items. Each has a stable id to use as the React key.
append(value) => voidAdds a new item to the end of the list.
prepend(value) => voidAdds a new item to the beginning of the list.
remove(index) => voidRemoves the item at the given index.
update(index, value) => voidReplaces the item at the given index.
move(from, to) => voidMoves an item from one position to another.

Example: Dynamic Contact List

The example below builds a form that lets the user add and remove contact entries, each containing a name and an email address.
import z from "zod";
import { ShaddyForm } from "@/components/form/shaddy-form";
import { FieldArray } from "@/components/form/field-array";
import { TextField } from "@/components/form/fields/text-field";
import { SubmitButton } from "@/components/form/fields/submit-button";
import { Button } from "@/components/ui/button";

const contactsSchema = z.object({
  contacts: z.array(
    z.object({
      name: z.string().min(1, { message: "Name is required" }),
      email: z.string().email({ message: "Invalid email address" }),
    })
  ).min(1, { message: "Add at least one contact" }),
});

type ContactsForm = z.infer<typeof contactsSchema>;

const initialValues: ContactsForm = {
  contacts: [{ name: "", email: "" }],
};

export function ContactListForm() {
  return (
    <ShaddyForm
      schema={contactsSchema}
      initialValues={initialValues}
      onSubmit={(data) => console.log("Contacts:", data)}
    >
      <div className="space-y-4 w-full max-w-lg">
        <FieldArray<ContactsForm> name="contacts">
          {({ fields, append, remove }) => (
            <div className="space-y-4">
              {fields.map((field, index) => (
                <div key={field.id} className="flex gap-2 items-start">
                  <div className="flex-1 space-y-2">
                    <TextField<ContactsForm>
                      name={`contacts.${index}.name`}
                      label="Name"
                      placeholder="Contact name"
                    />
                    <TextField<ContactsForm>
                      name={`contacts.${index}.email`}
                      label="Email"
                      placeholder="contact@example.com"
                    />
                  </div>
                  <Button
                    type="button"
                    variant="destructive"
                    size="sm"
                    className="mt-8"
                    onClick={() => remove(index)}
                    disabled={fields.length === 1}
                  >
                    Remove
                  </Button>
                </div>
              ))}

              <Button
                type="button"
                variant="outline"
                onClick={() => append({ name: "", email: "" })}
              >
                + Add Contact
              </Button>
            </div>
          )}
        </FieldArray>

        <SubmitButton label="Save Contacts" />
      </div>
    </ShaddyForm>
  );
}
Always use field.id (not index) as the React key for array items. react-hook-form assigns a stable, unique id to each entry that does not change when items are reordered or removed. Using index as a key causes subtle state bugs when the list changes.

Nesting FieldArrays

FieldArray can be nested to support multi-level dynamic structures. Ensure that each name prop reflects the correct dot-notation path to the nested array within the schema. Because TypeScript’s ArrayPath<T> and Path<T> do not accept template literal strings, you must cast dynamic index paths using as when building nested field names at runtime.
import type { ArrayPath, Path } from "react-hook-form";

<FieldArray<FormValues> name="sections">
  {({ fields: sections, append: appendSection }) =>
    sections.map((section, sectionIndex) => (
      <FieldArray<FormValues>
        key={section.id}
        name={`sections.${sectionIndex}.items` as ArrayPath<FormValues>}
      >
        {({ fields: items, append: appendItem }) =>
          items.map((item, itemIndex) => (
            <TextField<FormValues>
              key={item.id}
              name={
                `sections.${sectionIndex}.items.${itemIndex}.title` as Path<FormValues>
              }
              label="Item Title"
            />
          ))
        }
      </FieldArray>
    ))
  }
</FieldArray>

Build docs developers (and LLMs) love