From zenbu-powers
React Hook Form v7 (^7.51+) with @hookform/resolvers v3 (Zod) complete API reference. Covers useForm, register, Controller, useController, useFormContext/FormProvider, useWatch, useFieldArray, useFormState, handleSubmit, formState, reset, setValue, validation modes, Zod resolver integration, TypeScript types, and performance patterns. Use this skill whenever code imports from 'react-hook-form' or '@hookform/resolvers', uses useForm/register/Controller/useFieldArray/useWatch/useFormContext/FormProvider, or involves form validation, form state management, dynamic form fields, or schema-based validation with Zod in React.
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
Version: `react-hook-form ^7.51.0`, `@hookform/resolvers ^3.3.0`. NOT for v8.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Share bugs, ideas, or general feedback.
Version: react-hook-form ^7.51.0, @hookform/resolvers ^3.3.0. NOT for v8.
const {
register, unregister, formState, watch, handleSubmit,
reset, resetField, setError, clearErrors, setValue,
setFocus, getValues, getFieldState, trigger, control,
} = useForm<FormValues>(options?)
| Option | Type | Default | Description |
|---|---|---|---|
mode | 'onSubmit'|'onBlur'|'onChange'|'onTouched'|'all' | 'onSubmit' | Validation before first submit |
reValidateMode | 'onChange'|'onBlur'|'onSubmit' | 'onChange' | Re-validation after submit |
defaultValues | FieldValues | () => Promise<FieldValues> | - | Cached initial values; supports async |
values | FieldValues | - | Reactive external values (overwrites on change) |
errors | FieldErrors | - | Server errors (keep ref-stable!) |
resetOptions | KeepStateOptions | - | Behavior when values/defaultValues update |
resolver | Resolver | - | Schema validation (Zod, Yup, etc.) |
context | object | - | Mutable context for resolver's 2nd arg |
criteriaMode | 'firstError'|'all' | 'firstError' | One or all errors per field |
shouldFocusError | boolean | true | Focus first error on submit |
shouldUnregister | boolean | false | Remove values on unmount |
delayError | number | - | ms delay before showing errors |
disabled | boolean | false | Disable entire form |
validate | Function | - | Form-level validation |
Mode details: onSubmit = validate on submit only; onBlur = on blur; onChange = every keystroke (perf impact); onTouched = first blur then onChange; all = blur + change.
defaultValues rules: Avoid undefined (conflicts with controlled state). Cached on first render -- use reset() to update. Included in submission. Supports async function. No prototype objects (Moment, Luxon).
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Required'),
email: z.string().email(),
});
type FormValues = z.infer<typeof schema>;
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: '', email: '' },
});
Resolver + built-in validators (required, min) CANNOT coexist on same field. Error keys must be hierarchical: { items: [null, { name: err }] } not { 'items.1.name': err }.
const { onChange, onBlur, name, ref } = register(name: string, options?: RegisterOptions)
| Option | Type | Description |
|---|---|---|
required | boolean|string | Must have value |
maxLength/minLength | number|{value,message} | Character limits |
max/min | number|{value,message} | Numeric limits |
pattern | RegExp|{value,message} | Regex (avoid /g flag) |
validate | Fn|Record<string,Fn> | Custom sync/async validation |
valueAsNumber | boolean | Cast to Number before validation |
valueAsDate | boolean | Cast to Date before validation |
setValueAs | (v)=>T | Transform value (ignored if valueAsNumber/Date) |
disabled | boolean | Returns undefined, omits validation |
onChange/onBlur | (e)=>void | Custom handlers alongside RHF's |
shouldUnregister | boolean | Unregister on unmount (avoid with useFieldArray) |
deps | string|string[] | Re-validate dependent fields |
<input {...register('slug', { required: 'Required', pattern: { value: /^[a-z0-9-]+$/, message: 'kebab-case' } })} />
<select {...register('status')}><option value="draft">Draft</option></select>
<input type="number" {...register('order', { valueAsNumber: true })} />
<input {...register('confirm', { validate: (v, fv) => v === fv.password || 'Must match', deps: ['password'] })} />
Name format: name.first -> {name:{first:v}}, items.0.title -> {items:[{title:v}]}. Dot syntax only, NOT brackets.
Destructured for custom ref: const { ref, ...rest } = register('x'); <C inputRef={ref} {...rest} />
For controlled components (rich text, date pickers, custom selects).
<Controller control={control} name="content" rules={{ required: true }}
render={({ field, fieldState }) => (
<Editor value={field.value} onChange={field.onChange} onBlur={field.onBlur} ref={field.ref} />
)}
/>
useController for reusable controlled components:
const { field, fieldState: { error } } = useController({ name, control, rules: { required: true } });
Props: name (required), control (optional with FormProvider), rules, shouldUnregister, disabled, defaultValue (prefer useForm's defaultValues).
field returns: { onChange, onBlur, value, name, ref, disabled }. onChange value must NOT be undefined.
fieldState returns: { invalid, isTouched, isDirty, error }.
Proxy-based -- only accessed properties trigger re-renders.
| Property | Type | Description |
|---|---|---|
isDirty | boolean | Any field differs from defaultValues |
dirtyFields | object | Map of modified fields |
touchedFields | object | Map of interacted fields |
isSubmitted | boolean | Submitted (stays true until reset) |
isSubmitSuccessful | boolean | Submitted without runtime error |
isSubmitting | boolean | Currently submitting |
isLoading | boolean | Loading async defaultValues |
submitCount | number | Total submits |
isValid | boolean | No errors. setError does NOT affect this. |
errors | FieldErrors | Errors by field name |
Error shape: errors.field?.message, errors.field?.type, errors.nested?.child?.message, errors.items?.[0]?.name?.message, errors.items?.root?.message.
{errors.slug && <p className="text-xs text-red-600">{errors.slug.message}</p>}
handleSubmit(onValid: SubmitHandler<T>, onInvalid?: SubmitErrorHandler<T>)
<form onSubmit={handleSubmit(onSubmit, onError)}>
// Outside form: onClick={() => handleSubmit(onSubmit)()}
// isSubmitting managed automatically during async onSubmit
watch(); // all | watch('name'); // single
watch(['a','b']); // multi | watch((v,{name})=>{}) // callback (no re-render)
useWatch isolates re-renders to the subscribing component:
const val = useWatch({ control, name: 'price', defaultValue: 0 });
Share methods without prop drilling.
const methods = useForm<T>({ ... });
<FormProvider {...methods}><form>...</form></FormProvider>
// In nested component:
const { register, formState: { errors } } = useFormContext<T>();
const { fields, append, prepend, insert, swap, move, update, replace, remove } =
useFieldArray({ control, name: 'items' });
| Method | Signature | Description |
|---|---|---|
fields | (obj & {id:string})[] | Items with auto-id for key |
append | (obj|obj[], focusOpts?) | Add to end |
prepend | (obj|obj[], focusOpts?) | Add to start |
insert | (idx, obj|obj[], focusOpts?) | At position |
swap/move | (from, to) | Reorder |
update | (idx, obj) | Replace (unmounts/remounts) |
replace | (obj[]) | Replace entire array |
remove | (idx?|idx[]?) | Remove; no arg = all |
Rules: Use field.id as key (NOT index). Data must be complete (not {}). Don't stack operations in one handler. Flat arrays unsupported. Validation via rules prop, errors at errors?.items?.root.
{fields.map((field, index) => (
<div key={field.id}><input {...register(`items.${index}.name`)} /></div>
))}
setValue: setValue(name, value, { shouldValidate?, shouldDirty?, shouldTouch? })
setValue('status', 'published', { shouldDirty: true });
setValue('translations.zh-TW.title', 'New', { shouldDirty: true });
getValues: No re-render. getValues(), getValues('name'), getValues(['a','b'])
reset: reset(values?, keepStateOptions?). No args = reset to defaultValues. After reset(newValues), newValues become new defaults. Ref-stable, safe in useEffect deps.
reset({ slug: data.slug, status: data.status }); // load from API
KeepStateOptions: keepDirty, keepDirtyValues, keepErrors, keepIsSubmitted, keepTouched, keepValues, keepDefaultValues, keepSubmitCount.
resetField: resetField('name'), resetField('name', { defaultValue: 'x' })
setError('email', { type: 'server', message: 'Already exists' });
setError('root.serverError', { type: '500', message: 'Server error' });
// setError does NOT affect isValid
clearErrors(); clearErrors('email'); clearErrors(['a','b']);
await trigger(); await trigger('email'); await trigger(['a','b']); // returns boolean
Isolate formState subscriptions:
const { isSubmitting } = useFormState({ control });
import type { UseFormReturn, FieldValues, FieldErrors, FieldPath, Control,
SubmitHandler, RegisterOptions, UseControllerProps } from 'react-hook-form';
interface Props { control: Control<FormValues>; name: FieldPath<FormValues>; }
type Methods = UseFormReturn<FormValues>;
register (uncontrolled) over Controller.useWatch in children, not watch in parent.useFormState in isolated components (buttons, error summaries).mode: 'onChange' unless needed.const { reset } = useForm().reset() to update.undefined defaultValue causes controlled/uncontrolled warnings.undefined -- use readOnly to keep value.undefined.field.id, never index.setError does NOT affect isValid.{} -- use { required: false }.