rhf-dynamic-field-arrays
React Hook Form useFieldArray for dynamic list management with Zod validation. PROACTIVELY activate for: (1) creating dynamic form arrays with add/remove, (2) validating arrays with Zod min/max, (3) using field.id as key. Triggers: "field array", "useFieldArray", "dynamic form"
What this skill does
# React Hook Form Dynamic Field Arrays
## useFieldArray Hook
For forms with dynamic lists of inputs (team members, invoice items, etc.):
```typescript
import { useForm, useFieldArray } from 'react-hook-form';
const { control } = useForm<FormData>();
const { fields, append, prepend, remove, insert, move } = useFieldArray({
control, // Required: control from useForm
name: 'items', // Required: name of array field
});
```
## Complete Example: Team Members Form
```typescript
'use client'
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. Define schema with array validation
const teamSchema = z.object({
teamName: z.string().min(1, 'Team name required'),
members: z.array(
z.object({
name: z.string().min(1, 'Name required'),
email: z.string().email('Invalid email'),
role: z.enum(['developer', 'designer', 'manager']),
})
).min(1, 'At least one member required').max(10, 'Maximum 10 members'),
});
type TeamFormData = z.infer<typeof teamSchema>;
export function TeamForm() {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm<TeamFormData>({
resolver: zodResolver(teamSchema),
defaultValues: {
teamName: '',
members: [{ name: '', email: '', role: 'developer' }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'members',
});
const onSubmit = (data: TeamFormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div>
<label>Team Name</label>
<input {...register('teamName')} />
{errors.teamName && <p className="error">{errors.teamName.message}</p>}
</div>
<div>
<div className="flex justify-between items-center mb-4">
<h3>Team Members</h3>
<button
type="button"
onClick={() => append({ name: '', email: '', role: 'developer' })}
>
Add Member
</button>
</div>
{/* CRITICAL: Use field.id as key, NOT index */}
{fields.map((field, index) => (
<div key={field.id} className="border p-4 rounded mb-4">
<div className="grid grid-cols-3 gap-4">
<div>
<label>Name</label>
<input {...register(`members.${index}.name`)} />
{errors.members?.[index]?.name && (
<p className="error">{errors.members[index]?.name?.message}</p>
)}
</div>
<div>
<label>Email</label>
<input {...register(`members.${index}.email`)} />
{errors.members?.[index]?.email && (
<p className="error">{errors.members[index]?.email?.message}</p>
)}
</div>
<div>
<label>Role</label>
<select {...register(`members.${index}.role`)}>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
<option value="manager">Manager</option>
</select>
</div>
</div>
<button
type="button"
onClick={() => remove(index)}
disabled={fields.length === 1}
className="mt-2 text-red-600"
>
Remove
</button>
</div>
))}
{errors.members && (
<p className="error">{errors.members.message}</p>
)}
</div>
<button type="submit">Create Team</button>
</form>
);
}
```
## Array Manipulation Methods
```typescript
const { fields, append, prepend, remove, insert, move, swap } = useFieldArray({
control,
name: 'items',
});
// Add to end
append({ name: '', price: 0 });
// Add to beginning
prepend({ name: '', price: 0 });
// Remove by index
remove(2);
// Remove multiple
remove([0, 2, 4]);
// Insert at specific position
insert(2, { name: '', price: 0 });
// Move item from one position to another
move(2, 5);
// Swap two items
swap(2, 5);
// Replace entire array
fields.replace([{ name: 'Item 1', price: 10 }]);
```
## Zod Array Validation
```typescript
// Basic array
z.array(z.string())
// Array with min/max
z.array(z.string())
.min(1, 'At least one item required')
.max(5, 'Maximum 5 items')
// Array of objects
z.array(
z.object({
name: z.string().min(1, 'Required'),
quantity: z.number().int().min(1),
})
)
// Nonempty array
z.array(z.string()).nonempty('Cannot be empty')
```
## Nested Arrays
```typescript
const schema = z.object({
sections: z.array(
z.object({
title: z.string(),
items: z.array(
z.object({
name: z.string(),
value: z.number(),
})
),
})
),
});
// Nested useFieldArray
function NestedForm() {
const { control, register } = useForm<FormData>();
const { fields: sections } = useFieldArray({
control,
name: 'sections',
});
return (
<>
{sections.map((section, sectionIndex) => (
<div key={section.id}>
<input {...register(`sections.${sectionIndex}.title`)} />
<NestedItems control={control} sectionIndex={sectionIndex} />
</div>
))}
</>
);
}
function NestedItems({ control, sectionIndex }: Props) {
const { fields: items } = useFieldArray({
control,
name: `sections.${sectionIndex}.items`,
});
return (
<>
{items.map((item, itemIndex) => (
<div key={item.id}>
<input {...register(`sections.${sectionIndex}.items.${itemIndex}.name`)} />
</div>
))}
</>
);
}
```
## Anti-Patterns
### Using Index as Key
```typescript
// WRONG: Using index as key
{fields.map((field, index) => (
<div key={index}>...</div>
))}
// CORRECT: Using field.id as key
{fields.map((field, index) => (
<div key={field.id}>...</div>
))}
```
**Why**: Using index as key causes state to persist incorrectly when items are reordered or removed.
### Incorrect Zod Schema
```typescript
// WRONG: Not wrapping in z.array()
const schema = z.object({
items: z.object({ name: z.string() }) // Missing z.array()
});
// CORRECT: Wrapped in z.array()
const schema = z.object({
items: z.array(z.object({ name: z.string() }))
});
```
## Summary
- Use `useFieldArray` for dynamic lists
- MUST use `field.id` as key (not index)
- Array validation with `z.array()`
- Manipulation methods: append, remove, insert, move
- Access errors with `errors.arrayName?.[index]?.fieldName`
---
**Related Skills**: `rhf-zod-schema-integration`, `zustand-rhf-state-synchronization`
Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.