web-forms-react-hook-form-c633bc — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-forms-react-hook-form-c633bc (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide:registerbinds native inputs and keeps them uncontrolled;Controllerwraps components that hold their own value;useFieldArraydrives repeatable rows and is keyed onfield.id. Validation arrives either asregisterrules or as a schema throughresolver. Re-renders are the thing to watch:useWatchanduseFormStatesubscribe to named fields, whereaswatch()and a wideformStatedestructure subscribe to the whole form.
Detailed Resources:
register, error display and accessibility attributesController around a select, a date picker and a checkbox groupresolveruseFieldArray line items with a live totalFormStateSubscribe, useWatch exact and computevalues prop for async data, disabled, the <Form /> componenttrigger()register hands the ref to the form, the fieldstays uncontrolled, and typing re-renders nothing. Follow examples/core.md.
usable ref, so Controller supplies value and onChange and confines the re-render to that field. Follow examples/controlled-components.md.
resolver andthe field-level rules drop out. Follow examples/validation.md.
<critical_requirements>
Call `useForm<FormData>()` with a generic and with `defaultValues` for every field. The generic types each field path and the submit payload; the defaults mount every input controlled from the first render.
Key `useFieldArray` rows on `field.id`. It is the identity RHF assigns the row and it survives add, remove and reorder, which an array index does not.
Reach for `Controller` as soon as a component holds its own value. register needs a ref that reaches a native input, and a component that does not forward one never joins the form.
Set `mode` to `"onBlur"` or `"onTouched"`. The default "onSubmit" withholds feedback until the first submit, and "onChange" validates on every keystroke.
Pass schema validation through `resolver`, with the schema in its own module. A schema outside the component is testable on its own and reusable across forms, and the form keeps only the wiring.
</critical_requirements>
Auto-detection: react-hook-form, useForm, register, handleSubmit, formState, Controller, useFieldArray, useWatch, useFormContext, useFormState, FormProvider, FormStateSubscribe, SubmitHandler, resolver, shouldUnregister, valueAsNumber
Applies to:
Handled elsewhere:
resolver accepts a schema object and this skill only wires itin; how the schema states its rules is settled by whatever owns it.
valuesoption and fetches nothing itself.
<philosophy>
Form state lives outside React state. Inputs register themselves with the form and report through a ref, so a keystroke updates the form's own store without re-rendering the component that owns the field. Everything that reads form state — an error message, a computed total, a submit button — opts in by subscribing to a named slice, and a component that subscribes to nothing never re-renders.
This is why the wide reads cost so much. watch() in a render body and a formState destructure that pulls six properties both subscribe to the entire form, undoing the isolation the library was built for.
</philosophy>
<patterns>
The generic, mode and defaultValues together settle type safety, validation timing and controlled-input warnings.
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<ContactFormData>({
mode: "onBlur",
defaultValues: { name: "", email: "", message: "" },
});Full code: examples/core.md
Controller renders the field itself and hands it value and onChange. The test for which to reach for: a component whose ref forwards to a native input works with register, and anything else needs Controller.
<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>}
</>
)}
/>Full code: examples/controlled-components.md
fields carries a generated id per row, and that is the React key. Array-level rules go on the hook, and their errors land at errors.items.root.
const { fields, append, remove } = useFieldArray({ control, name: "items" });
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.name`)} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}Full code: examples/arrays.md
resolver replaces the per-field rules: the schema decides what is valid and reports errors against field paths, and the form does the wiring.
const { register, handleSubmit } = useForm<FormData>({
resolver: zodResolver(schema),
mode: "onBlur",
defaultValues: { username: "", email: "" },
});A schema kept in its own module is testable without rendering, reusable across forms, and can state cross-field rules — matching passwords, a date range — that per-field rules cannot express.
Full code: examples/validation.md
useWatch in a child component subscribes to named fields, so only that child re-renders when they change.
function PriceDisplay({ control }: { control: Control<PricingFormData> }) {
const [plan, seats] = useWatch({ control, name: ["plan", "seats"] });
return <div>Total: ${PLAN_PRICES[plan] * seats}</div>;
}The compute option narrows the subscription further — the component re-renders when the computed result changes rather than when an input to it does.
Full code: examples/performance.md Pattern 11 and Pattern 12
FormProvider puts the form methods on context so a nested or reused section reaches them without prop drilling. Worth it at three levels of nesting or for a section rendered more than once; below that, passing control is simpler.
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<AddressFields prefix="shippingAddress" />
<AddressFields prefix="billingAddress" />
</form>
</FormProvider>
function AddressFields({ prefix }) {
const { register } = useFormContext<CheckoutFormData>();
return <input {...register(`${prefix}.street`)} />;
}Full code: examples/wizard.md
values is reactive — the form follows the data as it changes — while defaultValues is read once on mount. Pair values with resetOptions: { keepDirtyValues: true } so a background refresh does not discard what the user has typed.
useForm<FormData>({
values: userData,
resetOptions: { keepDirtyValues: true },
});After a successful save, reset(data) replaces the values and the defaults together, which is what clears isDirty. reset() with no argument reverts to the original defaults — the cancel button.
Full code: examples/form-options.md Pattern 7
useFormState with a name re-renders only when that field's state changes, which keeps an error message from re-rendering the form around it.
function FieldError<T extends FieldValues>({ control, name }: Props<T>) {
const { errors } = useFormState({ control, name });
const error = errors[name];
if (!error) return null;
return <span role="alert">{error.message as string}</span>;
}Full code: examples/performance.md — Pattern 5 for the hook, Pattern 10 for the FormStateSubscribe component form
</patterns>
<red_flags>
Breaks at runtime:
useFieldArray key — React matches the wrong rows, so removing a middleitem shifts every value below it up one. Key on field.id.
register on a component that holds its own value — no ref arrives, the field never registers,and its value is absent from the submit payload. Wrap it in Controller.
defaultValues — inputs mount uncontrolled and flip to controlled on the first keystroke,which React warns about and SSR reports as a hydration mismatch. Seed every field, "" included.
append, prepend or insert — the absent keys arrive as undefinedand their inputs read as uncontrolled. Pass a complete item.
onSubmit — handleSubmit does not catch it, so the rejection escapesunhandled. Catch inside the callback.
setValue against a field array's own name — the row ids do not move with the values. Usereplace().
Surprising behaviour:
useForm leaves field paths and the submit payload as any, so register("emial")is accepted in silence.
formState properties subscribes to all of them, and the form thenre-renders on any change. Take only what the component reads, or isolate it with useFormState.
watch() in a render body subscribes to every field; useWatch in a child narrows it to named ones.setValue without shouldValidate: true leaves the previous error on screen.errors.items.root; per-item errors at errors.items[index].field.shouldUnregister: true discards the values of unmounted fields. Leave it false (the default)for anything that hides fields, wizards especially.
useWatch returns its defaultValue on the first render, before the subscription attaches.values is for external data that keeps changing and defaultValues for static initial values;supplying both makes which one wins depend on resetOptions.
trigger(fieldNames) — isValid reflects the whole form, so a wizardgated on it is stuck on step one.
Worked before/after code for the most common of these is in reference.md.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.