Skip to content

/web-forms-react-hook-form

React Hook Form patterns - useForm, Controller, useFieldArray, validation resolver, performance optimization

shell
$ npx -y skills add agents-inc/skills --skill web-forms-react-hook-form --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/web-forms-react-hook-form
How auto-invocation works

Context preview

The summary Claude sees to decide when to auto-load this skill.

React Hook Form patterns - useForm, Controller, useFieldArray, validation resolver, performance optimization

SKILL.md

web-forms-react-hook-form.SKILL.md
name: web-forms-react-hook-form
description: React Hook Form patterns - useForm, Controller, useFieldArray, validation resolver, performance optimization

React Hook Form Patterns

> **Quick Guide:** Use `register` for native inputs, `Controller` for controlled components, `useFieldArray` for dynamic fields. Always provide `useForm<FormData>()` generics. Set `mode: "onBlur"` for optimal UX. Use resolver pattern for schema validation. Use `useWatch` instead of `watch()` in render to avoid re-rendering the whole form. Use `field.id` as key in useFieldArray -- never array index.

---

<critical_requirements>

CRITICAL: Before Using This Skill

> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)

**(You MUST provide generic types to `useForm<FormData>()` for type-safe form handling)**

**(You MUST use `field.id` as key prop in useFieldArray - NEVER use array index)**

**(You MUST use Controller for controlled components that don't expose a ref)**

**(You MUST use resolver pattern for schema validation - keep schemas separate from form logic)**

**(You MUST set `mode: "onBlur"` or `mode: "onTouched"` for optimal UX - avoid `mode: "onChange"` unless needed)**

</critical_requirements>

---

**Auto-detection:** React Hook Form, useForm, register, handleSubmit, formState, Controller, useFieldArray, useWatch, useFormContext, resolver, zodResolver, FormProvider, useFormState, FormStateSubscribe

**When to use:**

  • Building forms with validation requirements
  • Managing complex form state with many fields
  • Creating dynamic forms with add/remove fields
  • Integrating with controlled component libraries
  • Handling multi-step or wizard forms

**When NOT to use:**

  • Single input without validation (use useState)
  • Server-only forms with server actions (use native form + action)
  • Read-only data display (not a form scenario)

**Key patterns covered:**

  • useForm hook with TypeScript generics
  • register vs Controller decision
  • useFieldArray for dynamic fields
  • Resolver integration for schema validation
  • useWatch and useFormState for performance
  • FormProvider/useFormContext for nested components
  • Form reset, async data loading, and `values` prop
  • FormStateSubscribe for targeted re-renders (v7.68+)

---

<philosophy>

Philosophy

React Hook Form prioritizes performance through uncontrolled inputs and subscription-based updates. Only fields that change re-render, not the entire form. The library isolates form state from component state, minimizing re-renders and keeping forms responsive even with many fields.

**Core Principles:**

1. **Uncontrolled by default** - Use `register` for native inputs to avoid re-renders 2. **Controlled when needed** - Use `Controller` for UI library components that don't expose a ref 3. **Schema validation via resolver** - Separate validation logic from form logic 4. **Subscription-based** - Subscribe to only the form state you need (`useWatch`, `useFormState`) 5. **Type safety** - Always provide TypeScript generics for form data

</philosophy>

---

<patterns>

Core Patterns

Pattern 1: Basic useForm with TypeScript

Always provide a type parameter, `mode`, and `defaultValues`. These three prevent the most common issues (no type safety, validation noise, undefined warnings).

const {
  register,
  handleSubmit,
  formState: { errors, isSubmitting },
} = useForm<ContactFormData>({
  mode: "onBlur",
  defaultValues: { name: "", email: "", message: "" },
});

**Why this matters:** Without generics, field names are `any`. Without `defaultValues`, values are `undefined` and cause hydration mismatches. Without `mode: "onBlur"`, the default `"onSubmit"` gives no feedback until first submit.

See [examples/core.md](examples/core.md) for complete form with accessibility attributes and error display.

---

Pattern 2: Controller for Controlled Components

Use `Controller` when a component doesn't expose a native ref (custom selects, date pickers, rich text editors). Use `register` for standard HTML inputs.

<Controller
  name="service"
  control={control}
  rules={{ required: "Service is required" }}
  render={({ field, fieldState: { error } }) => (
    <>
      <Select {...field} options={serviceOptions} />
      {error && <span role="alert">{error.message}</span>}
    </>
  )}
/>

**Key decision:** If the component accepts a `ref` prop that forwards to a native input, `register` works. Otherwise, use `Controller`.

See [examples/controlled-components.md](examples/controlled-components.md) for single select, date picker, and multi-select checkbox patterns.

---

Pattern 3: useFieldArray for Dynamic Fields

Use `useFieldArray` for repeatable field groups. **Always use `field.id` as the React key** -- array index causes state corruption on add/remove.

const { fields, append, remove } = useFieldArray({ control, name: "items" });

{fields.map((field, index) => (
  <div key={field.id}> {/* CRITICAL: field.id, never index */}
    <input {...register(`items.${index}.name`)} />
    <button type="button" onClick={() => remove(index)}>Remove</button>
  </div>
))}

**Gotcha:** `append`/`prepend`/`insert` require complete objects (not partial). Use `rules.minLength` on `useFieldArray` for minimum item validation. Array-level errors live at `errors.items.root`.

See [examples/arrays.md](examples/arrays.md) for a complete invoice form with calculated totals.

---

Pattern 4: Resolver for Schema Validation

Use `resolver` to integrate validation schemas. The resolver handles validation; you wire it to the form. Keep schema definition separate from form code.

import { zodResolver } from "@hookform/resolvers/zod";

const { register, handleSubmit } = useForm<FormData>({
  resolver: zodResolver(schema),
  mode: "onBlur",
  defaultValues: { username: "", email: "" },
});

**Why resolver over inline rules:** S

Read more
Read it on GitHub ↗

Showing the first part of this file.

Ships withagents-inc-skills

The official skills marketplace for Agents Inc. 150+ skills covering everything from React and Prisma to Redis, ElevenLabs, and infrastructure tooling. Pick the skills that match your stack and install them via Claude Code. Need more control?

Get the whole plugin, auto-invoked