Skip to content

/web-forms-tanstack-form

TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety

shell
$ npx -y skills add agents-inc/skills --skill web-forms-tanstack-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-tanstack-form
How auto-invocation works

Context preview

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

TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety

SKILL.md

web-forms-tanstack-form.SKILL.md
name: web-forms-tanstack-form
description: TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety

TanStack Form Patterns

> **Quick Guide:** Use `useForm` with `defaultValues` and typed generics. Render fields with `form.Field` using the render-prop `children` pattern. Validation lives in the `validators` prop on both form and field level — use `onChange`, `onBlur`, `onSubmit` (sync) and their `Async` variants. Use `mode="array"` for dynamic field lists with `pushValue`/`removeValue`. Use `onChangeListenTo` for cross-field validation. For app-wide consistency, create a shared `useAppForm` via `createFormHook`. Always provide `defaultValues` — TanStack Form infers types from them.

---

<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 `defaultValues` to `useForm` — TanStack Form infers field types from them)**

**(You MUST use `form.Field` with the `children` render prop — TanStack Form does not use `register` or `Controller`)**

**(You MUST use the `validators` prop for validation — NOT inline `rules` or external resolver wrappers)**

**(You MUST handle `field.state.meta.errors` as an array — always `.map()` over errors)**

**(You MUST call `form.handleSubmit()` inside the form's `onSubmit` handler with `e.preventDefault()`)**

</critical_requirements>

---

**Auto-detection:** TanStack Form, @tanstack/react-form, @tanstack/vue-form, @tanstack/solid-form, @tanstack/angular-form, @tanstack/lit-form, useForm from tanstack, form.Field, createFormHook, createFormHookContexts, useAppForm, fieldContext, formContext, handleSubmit tanstack, pushValue, removeValue, onChangeListenTo, field.handleChange, field.handleBlur, field.state, formDevtoolsPlugin

**When to use:**

  • Building type-safe forms where field types are inferred from `defaultValues`
  • Managing complex validation with sync, async, and cross-field rules
  • Dynamic forms with add/remove field groups (array fields)
  • Multi-framework projects (React, Vue, Solid, Angular, Lit)
  • Projects already using the TanStack ecosystem

**When NOT to use:**

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

---

Table of Contents

**Detailed Resources:**

  • [examples/core.md](examples/core.md) - Basic form, Field component, TypeScript, form submission
  • [examples/validation.md](examples/validation.md) - Sync/async validation, validator adapters, form-level validation
  • [examples/arrays.md](examples/arrays.md) - Dynamic array fields with pushValue/removeValue
  • [examples/composition.md](examples/composition.md) - createFormHook, useAppForm, listeners, side effects
  • [reference.md](reference.md) - API tables, validator events, decision frameworks

---

<philosophy>

Philosophy

TanStack Form is **headless and type-safe by design**. It owns zero UI — you render every input yourself. The library provides form state, validation orchestration, and field management. Types flow from `defaultValues` through every field name, value, and error — no manual generics required (though you can provide them).

**Core Principles:**

1. **Type inference from defaults** - `defaultValues` defines the form shape; field names and values are fully typed 2. **Headless** - Zero UI opinions; works with any component library or native inputs 3. **Validation-event-driven** - Validators attach to specific events (`onChange`, `onBlur`, `onSubmit`) per field or per form 4. **Framework-agnostic core** - Same mental model across React, Vue, Solid, Angular, and Lit 5. **Composition via factory** - `createFormHook` shares field/form components across an app

</philosophy>

---

<patterns>

Core Patterns

Pattern 1: Basic useForm + form.Field

Every form starts with `useForm` and renders fields via `form.Field`. The `children` render prop receives the field API with `state`, `handleChange`, and `handleBlur`.

import { useForm } from "@tanstack/react-form";

const form = useForm({
  defaultValues: { name: "", email: "" },
  onSubmit: async ({ value }) => {
    await submitToApi(value);
  },
});

return (
  <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>
);

**Key difference from other form libraries:** No `register`, no `Controller`, no `ref` forwarding. You always use `field.handleChange` and `field.state.value` explicitly.

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

---

Pattern 2: Field-Level Validation

Validators are functions on the `validators` prop. Sync validators return a string (error) or `undefined` (valid). Async validators use `onChangeAsync`, `onBlurAsync`, `onSubmitAsync`.

<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => (value < 13 ? "Must be 13 or older" : undefined),
    onBlurAsync: async ({ value }) => {
      const exists = await checkAge(value);
      return exists ? undefined : "Age not valid on server";
    },
  }}
  children={(field) => (
    <div>
      <input
        type="number"
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.valueAsNumber)}
      />
      {field.state.meta.errors.map((err) => (
        <em key={err} role="alert">
          {err}
        </em>
      ))}
    </div>
  )}
/>

**Sync-first gating:** When both `onBlur` and `onBlurAsync` exist, the async validator only runs if the sync validator passes. Same for `onChange`/

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