AgentSkillsCN

tanstack-form

使用 TanStack Form 与 React Aria Components 构建表单。适用于构建表单、实现验证、处理表单提交,或创建可复用的表单字段时使用。

SKILL.md
--- frontmatter
name: tanstack-form
description: Create forms with TanStack Form and React Aria Components. Use when building forms, implementing validation, handling form submissions, or creating reusable form fields.

TanStack Form

Quick Start

sh
pnpm add @tanstack/react-form

Two Patterns

PatternUse CaseAPI
BasicOne-off formsuseForm + form.Field
ComposableReusable fieldscreateFormHook + form.AppField

Basic Pattern

tsx
import { useForm } from '@tanstack/react-form';
import { z } from 'zod';
import { TextField, Button } from '@oakoss/ui';

const schema = z.object({
  email: z.email(),
  name: z.string().min(1),
});

function MyForm() {
  const form = useForm({
    defaultValues: { email: '', name: '' },
    validators: { onSubmit: schema },
    onSubmit: async ({ value }) => console.log(value),
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
    >
      <form.Field
        name="email"
        children={(field) => {
          const isInvalid =
            field.state.meta.isTouched && !field.state.meta.isValid;
          return (
            <TextField
              label="Email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(value) => field.handleChange(value)}
              isInvalid={isInvalid}
              errorMessage={
                isInvalid ? field.state.meta.errors.join(', ') : undefined
              }
            />
          );
        }}
      />
      <Button type="submit">Submit</Button>
    </form>
  );
}

Composable Pattern (Recommended)

1. Create contexts

tsx
// apps/web/src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/react-form';
export const { fieldContext, formContext, useFieldContext, useFormContext } =
  createFormHookContexts();

2. Create field component

tsx
// apps/web/src/components/form/text-field.tsx
import { TextField as AriaTextField } from '@oakoss/ui';
import { useFieldContext } from '@/hooks/form-context';
import { useStore } from '@tanstack/react-form';

export function FormTextField({ label }: { label: string }) {
  const field = useFieldContext<string>();
  const errors = useStore(field.store, (s) => s.meta.errors);
  const isInvalid = field.state.meta.isTouched && errors.length > 0;

  return (
    <AriaTextField
      label={label}
      value={field.state.value}
      onBlur={field.handleBlur}
      onChange={(value) => field.handleChange(value)}
      isInvalid={isInvalid}
      errorMessage={isInvalid ? errors.join(', ') : undefined}
    />
  );
}

3. Create form hook

tsx
// apps/web/src/hooks/use-app-form.ts
import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from './form-context';
import {
  FormTextField,
  FormSelectField,
  SubmitButton,
} from '@/components/form';

export const { useAppForm } = createFormHook({
  fieldComponents: { TextField: FormTextField, SelectField: FormSelectField },
  formComponents: { SubmitButton },
  fieldContext,
  formContext,
});

4. Use in forms

tsx
const form = useAppForm({ defaultValues: { name: '' }, onSubmit });

<form.AppField name="name" children={(f) => <f.TextField label="Name" />} />
<form.AppForm><form.SubmitButton label="Submit" /></form.AppForm>

Validation

tsx
// Form-level with Zod (or Valibot, ArkType, Yup)
validators: {
  onSubmit: schema;
}

// Field-level
<form.Field
  name="email"
  validators={{
    onChange: z.email(),
    onChangeAsyncDebounceMs: 500,
    onChangeAsync: async ({ value }) => checkAvailable(value),
  }}
/>;

// Linked fields - re-validate when another field changes
<form.Field
  name="confirmPassword"
  validators={{
    onChangeListenTo: ['password'],
    onChange: ({ value, fieldApi }) =>
      value !== fieldApi.form.getFieldValue('password')
        ? 'Passwords must match'
        : undefined,
  }}
/>;

Form State

tsx
form.state.values; // Current values
form.state.isSubmitting;
form.state.isValid;
form.state.isDirty;
form.reset(); // Reset to defaults
form.handleSubmit(); // Submit programmatically

Common Mistakes

MistakeCorrect Pattern
Using e.target.value directlyUse field.handleChange(e.target.value)
Missing onBlur={field.handleBlur}Always add for validation timing
Not showing validation errorsCheck field.state.meta.isTouched && errors.length
Missing id on inputsUse id={field.name} for label association
Missing aria-invalidAdd for accessibility
Using interface keywordUse type for form value types
Using deprecated Zod syntaxUse z.email() not z.string().email()
Server validation onlyAlways validate client-side first

Delegation

  • Form pattern discovery: For finding existing form implementations, use Explore agent
  • Code review: After creating forms, delegate to code-reviewer agent

Topic References