agentleFS
dvaJidvaJi/argos/.agents/skills/tanstack-form/SKILL.md

tanstack-form

Skill3 starsChanged 2 months agoApache-2.0View on GitHub

Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.

  • Installs packages

4 other projects use a version of this, 1 of them edited.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Not attaching onBlur to inputs (breaks blur validation and isTouched)

    line 412
  2. Very specific:

    Not using asyncDebounceMs with async validators (fires on every keystroke)

    line 416

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Overview

TanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).

Package: @tanstack/react-form Adapters: @tanstack/zod-form-adapter, @tanstack/valibot-form-adapter Status: Stable (v1)

Installation

npm install @tanstack/react-form
# Optional schema adapters:
npm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibot

Core: useForm

import { useForm } from '@tanstack/react-form'

function MyForm() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      age: 0,
    },
    onSubmit: async ({ value }) => {
      // value is fully typed
      await submitToServer(value)
    },
    onSubmitInvalid: ({ value, formApi }) => {
      console.log('Validation failed:', formApi.state.errors)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        form.handleSubmit()
      }}
    >
      {/* Fields */}
      <form.Subscribe
        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
        children={({ canSubmit, isSubmitting }) => (
          <button type="submit" disabled={!canSubmit}>
            {isSubmitting ? 'Submitting...' : 'Submit'}
          </button>
        )}
      />
    </form>
  )
}

Fields (form.Field)

<form.Field
  name="firstName"
  validators={{
    onChange: ({ value }) =>
      value.length < 3 ? 'Must be at least 3 characters' : undefined,
  }}
  children={(field) => (
    <div>
      <label htmlFor={field.name}>First Name</label>
      <input
        id={field.name}
        name={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
        <em>{field.state.meta.errors.join(', ')}</em>
      )}
    </div>
  )}
/>

<!-- Nested fields use dot notation -->
<form.Field name="address.city">
  {(field) => (
    <input
      value={field.state.value}
      onChange={(e) => field.handleChange(e.target.value)}
      onBlur={field.handleBlur}
    />
  )}
</form.Field>

Validation

Validation Timing

CauseWhen
onChangeAfter every value change
onBlurWhen field loses focus
onSubmitDuring submission
onMountWhen field mounts

Synchronous Validation

<form.Field
  name="age"
  validators={{
    onChange: ({ value }) => {
      if (value < 18) return 'Must be 18 or older'
      return undefined // undefined = valid
    },
    onBlur: ({ value }) => {
      if (!value) return 'Required'
      return undefined
    },
  }}
/>

Asynchronous Validation

<form.Field
  name="username"
  asyncDebounceMs={500}
  validators={{
    onChangeAsync: async ({ value }) => {
      const res = await fetch(`/api/check-username?q=${value}`)
      const { available } = await res.json()
      if (!available) return 'Username taken'
      return undefined
    },
  }}
>
  {(field) => (
    <>
      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
      {field.state.meta.isValidating && <span>Checking...</span>}
    </>
  )}
</form.Field>

Schema Validation (Zod)

import { zodValidator } from '@tanstack/zod-form-adapter'
import { z } from 'zod'

const form = useForm({
  defaultValues: { email: '', age: 0 },
  validatorAdapter: zodValidator(),
  onSubmit: async ({ value }) => { /* ... */ },
})

<form.Field
  name="email"
  validators={{
    onChange: z.string().email('Invalid email'),
    onBlur: z.string().min(1, 'Required'),
  }}
/>

<form.Field
  name="age"
  validators={{
    onChange: z.number().min(18, 'Must be 18+'),
  }}
/>

Form-Level Validation

const form = useForm({
  defaultValues: { password: '', confirmPassword: '' },
  validators: {
    onChange: ({ value }) => {
      if (value.password !== value.confirmPassword) {
        return 'Passwords do not match'
      }
      return undefined
    },
  },
})

Linked/Dependent Fields

<form.Field
  name="confirmPassword"
  validators={{
    onChangeListenTo: ['password'], // Re-validate when password changes
    onChange: ({ value, fieldApi }) => {
      const password = fieldApi.form.getFieldValue('password')
      if (value !== password) return 'Passwords do not match'
      return undefined
    },
  }}
/>

Array Fields

<form.Field name="people" mode="array">
  {(field) => (
    <div>
      {field.state.value.map((_, index) => (
        <div key={index}>
          <form.Field name={`people[${index}].name`}>
            {(subField) => (
              <input
                value={subField.state.value}
                onChange={(e) => subField.handleChange(e.target.value)}
              />
            )}
          </form.Field>
          <button type="button" onClick={() => field.removeValue(index)}>
            Remove
          </button>
        </div>
      ))}
      <button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
        Add Person
      </button>
    </div>
  )}
</form.Field>

Array Methods

field.pushValue(item)              // Add to end
field.insertValue(index, item)     // Insert at index
field.replaceValue(index, item)    // Replace at index
field.removeValue(index)           // Remove at index
field.swapValues(indexA, indexB)    // Swap positions
field.moveValue(from, to)          // Move position

Listeners (Side Effects)

<form.Field
  name="country"
  listeners={{
    onChange: ({ value }) => {
      // Side effect: reset dependent fields
      form.setFieldValue('state', '')
      form.setFieldValue('postalCode', '')
    },
  }}
/>

Reactivity (form.Subscribe & useStore)

// Render-prop subscription (fine-grained)
<form.Subscribe
  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
  children={({ canSubmit, isDirty }) => (
    <div>
      {isDirty && <span>Unsaved changes</span>}
      <button disabled={!canSubmit}>Save</button>
    </div>
  )}
/>

// Hook-based subscription
function FormStatus() {
  const isValid = form.useStore((s) => s.isValid)
  return isValid ? null : <p>Fix errors</p>
}

Form State

interface FormState {
  values: TFormData
  errors: ValidationError[]
  errorMap: Record<string, ValidationError>
  isFormValid: boolean
  isFieldsValid: boolean
  isValid: boolean               // isFormValid && isFieldsValid
  isTouched: boolean
  isPristine: boolean
  isDirty: boolean
  isSubmitting: boolean
  isSubmitted: boolean
  isSubmitSuccessful: boolean
  submissionAttempts: number
  canSubmit: boolean             // isValid && !isSubmitting
}

Field State

interface FieldState<TData> {
  value: TData
  meta: {
    isTouched: boolean
    isDirty: boolean
    isPristine: boolean
    isValidating: boolean
    errors: ValidationError[]
    errorMap: Record<ValidationCause, ValidationError>
  }
}

FormApi Methods

form.handleSubmit()
form.reset()
form.getFieldValue(field)
form.setFieldValue(field, value)
form.getFieldMeta(field)
form.setFieldMeta(field, updater)
form.validateAllFields(cause)
form.validateField(field, cause)
form.deleteField(field)

Shared Form Options (formOptions)

import { formOptions } from '@tanstack/react-form'

const sharedOpts = formOptions({
  defaultValues: { firstName: '', lastName: '' },
})

// Reuse across components
const form = useForm({
  ...sharedOpts,
  onSubmit: async ({ value }) => { /* ... */ },
})

Server-Side Validation

// TanStack Start / Next.js server action
import { ServerValidateError } from '@tanstack/react-form/nextjs'

export async function validateForm(data: FormData) {
  const email = data.get('email') as string
  if (await checkEmailExists(email)) {
    throw new ServerValidateError({
      form: 'Submission failed',
      fields: { email: 'Email already registered' },
    })
  }
}

TypeScript Integration

// Type-safe field paths with DeepKeys
interface UserForm {
  name: string
  address: { street: string; city: string }
  tags: string[]
  contacts: Array<{ name: string; phone: string }>
}

// TypeScript auto-completes all valid paths:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
<form.Field name="address.city" />     // OK
<form.Field name="nonexistent" />       // Type Error!

Best Practices

  1. Always call e.preventDefault() and e.stopPropagation() on form submit
  2. Always attach onBlur={field.handleBlur} for blur validation and isTouched tracking
  3. Use mode="array" for array fields to get array methods
  4. Return undefined (not null/false) for valid validators
  5. Use asyncDebounceMs for async validators to prevent API spam
  6. Check isTouched before showing errors for better UX
  7. Use form.Subscribe with selectors to minimize re-renders
  8. Use formOptions for shared configuration across components
  9. Use schema validators (Zod/Valibot) for complex validation rules
  10. Use onChangeListenTo for cross-field validation dependencies

Common Pitfalls

  • Forgetting e.preventDefault() on form submit (causes page reload)
  • Not attaching onBlur to inputs (breaks blur validation and isTouched)
  • Returning null or false instead of undefined for valid fields
  • Using mode="array" incorrectly (only needed on the array field itself, not sub-fields)
  • Subscribing to entire form state instead of using selectors (unnecessary re-renders)
  • Not using asyncDebounceMs with async validators (fires on every keystroke)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.