web-forms-tanstack-form-fbf0ad — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-forms-tanstack-form-fbf0ad (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:useFormtakesdefaultValues, and every field name, value type and the submit payload are inferred from that object. Fields render throughform.Fieldwith achildrenrender prop that suppliesfield.state.value,field.handleChangeandfield.handleBlur. Validation lives in thevalidatorsprop — keyed by event (onChange,onBlur,onSubmit) with anAsyncvariant of each, on the field or on the form.mode="array"unlockspushValue/removeValue,onChangeListenTore-runs a validator when another field changes, andform.Subscribenarrows which state changes re-render what.
Detailed Resources:
mode="array"createFormHook, useAppForm, listenersuseForm plus form.Field render props, nothing else to set up. FollowcreateFormHook registers shared field andform components once, and useAppForm replaces useForm at each call site. Follow examples/composition.md.
binding differ; reference.md's Framework Packages table names both for each.
<critical_requirements>
Give `useForm` a `defaultValues` entry for every field. Field names, value types and the submit payload are all inferred from that object, so a field missing from it is a field the types do not know about.
Render every field through `form.Field` and its `children` render prop. The render prop receives the value and the handlers explicitly — this library has no field-registration helper and does no ref forwarding, so an input wired any other way never joins the form.
Put validation in the `validators` prop, keyed by the event that should run it. onChange, onBlur and onSubmit each have an Async counterpart, and the same prop exists on the field and on the form.
Read `field.state.meta.errors` as an array. It holds every current error for the field, so .map() over it or check .length; compared against a string it is always unequal.
Call `e.preventDefault()` in the form's `onSubmit` before `form.handleSubmit()`. The library does not intercept the native submit, so without it the browser navigates away mid-submission.
</critical_requirements>
Auto-detection: @tanstack/react-form, @tanstack/vue-form, @tanstack/solid-form, @tanstack/angular-form, @tanstack/lit-form, @tanstack/form-core, form.Field, form.Subscribe, createFormHook, createFormHookContexts, useAppForm, withForm, fieldContext, formContext, field.handleChange, field.handleBlur, field.state.meta, pushValue, removeValue, swapValues, onChangeListenTo, onBlurListenTo, setErrorMap, formDevtoolsPlugin
Applies to:
Handled elsewhere:
schema states its rules is settled by whatever owns it.
and the handlers, and the markup is yours.
defaultValues is a plain object, and the form fetchesnothing.
<philosophy>
The form is headless and its types run on inference. defaultValues is the schema of record: field names autocomplete from it, field.state.value is typed by it, and the onSubmit payload matches it — without a generic parameter, and without a second type declaration that could drift.
Validation is bound to events rather than to a mode. Each validator declares when it runs, at the level it belongs to, so a cheap format check can sit on onChange while the expensive uniqueness check waits for onBlurAsync on the same field.
State is read by subscription. form.Subscribe and useStore take a selector and re-render only when what the selector returns changes, so reading form.state directly in a component body opts out of the whole design.
</philosophy>
<patterns>
The render prop is the whole field API — value in, handlers out, nothing implicit.
const form = useForm({
defaultValues: { name: "", email: "" },
onSubmit: async ({ value }) => {
await submitToApi(value);
},
});
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="email"
children={(field) => (
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</form>;onBlur={field.handleBlur} is what marks the field touched — omit it and isTouched stays false and any onBlur validator never runs.
Full code: examples/core.md
A sync validator returns a message string, or undefined when the value passes.
<form.Field
name="age"
validators={{
onChange: ({ value }) => (value < 13 ? "Must be 13 or older" : undefined),
onBlurAsync: async ({ value }) => {
const ok = await checkAge(value);
return ok ? undefined : "Age not valid on server";
},
}}
children={(field) => (/* ... */)}
/>Sync gates async: when onBlur and onBlurAsync are both present, the async one runs only after the sync one passes — so a network call never fires on a value already known to be invalid.
Full code: examples/validation.md
onChangeListenTo names the fields whose changes should re-run this field's validators.
<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ["password"],
onChange: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue("password")
? "Passwords do not match"
: undefined,
}}
children={(field) => (/* ... */)}
/>Without it, editing password leaves the error on confirm_password showing the verdict from the old comparison until the user touches the confirm field again.
Full code: examples/validation.md
mode="array" gives the field pushValue, removeValue, insertValue, swapValues and moveValue. Nested fields address items by index.
<form.Field
name="hobbies"
mode="array"
children={(hobbies) => (
<div>
{hobbies.state.value.map((_, i) => (
<form.Field
key={i}
name={`hobbies[${i}].name`}
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
))}
<button type="button" onClick={() => hobbies.pushValue({ name: "" })}>
Add hobby
</button>
</div>
)}
/>Full code: examples/arrays.md
Validators on useForm see every value at once, which is where server-side validation belongs because it can attribute errors back to individual fields.
const form = useForm({
defaultValues: { username: "", age: 0 },
validators: {
onSubmitAsync: async ({ value }) => {
const errors = await validateOnServer(value);
if (!errors) return null;
return {
form: "Submission failed",
fields: { username: errors.username, age: errors.age },
};
},
},
});The return shape is { form?: string, fields: Record<string, string> }, and null means valid. This differs from a field validator, which returns a bare string.
Full code: examples/validation.md
The factory registers field and form components once, so each form reaches them as form.AppField and form.AppForm instead of repeating the render-prop markup.
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts();
export const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TextField, SelectField },
formComponents: { SubmitButton },
});useAppForm accepts everything useForm does.
Full code: examples/composition.md
Listeners react to a field event and cause an effect. They return nothing — a validator is what returns errors.
<form.Field
name="country"
listeners={{
onChange: () => form.setFieldValue("province", ""),
}}
children={(field) => (/* ... */)}
/>Available events: onChange, onBlur, onMount, onSubmit.
Full code: examples/composition.md
The selector decides what re-renders. Narrow it to the state actually rendered.
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting] as const}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
)}
/>Full code: examples/core.md
</patterns>
<red_flags>
Breaks at runtime:
form.handleSubmit() without e.preventDefault() — the browser submits the form natively and thepage reloads mid-submission.
defaultValues missing a field — its field.state.value is undefined, the input mountsuncontrolled, and the field's type is unknown.
field.state.meta.errors compared as a string — it is an array, so the comparison is always falseand the message never renders. .map() over it.
pushValue — it does not match the array's element type, and theabsent keys leave their inputs uncontrolled.
onSubmit — form.handleSubmit() does not catch it. Catch inside thecallback and surface it with form.setErrorMap().
items[0].name, and items.0.name addressesnothing.
Surprising behaviour:
form.state read in a component body subscribes to every state change. form.Subscribe with aselector, or useStore(form.store, selector), narrows it.
form.Subscribe with no selector subscribes to everything, which is the same cost.an async validator alone carries no cheap pre-check.
{ form?, fields } while a field validator returns a string — thefield shape returned from the form level is ignored in silence.
createFormHook live on form.AppField and form.AppForm;form.Field still exists and still takes a plain render prop.
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.