Skip to content
Automation
Skill

/tanstack-form-composition

Migrate a React @tanstack/react-form codebase from the prop-drilled `useForm` + erased-form-type pattern to the official `createFormHook` composition API (`useAppForm` / `withForm` / `field.X`). Use when a project threads a `form` object (often cast to an `any`-erased type like

From plugin
jobpilot
7331 skills2 agents2 MCP
Install
$ npx -y skills add suxrobGM/jobpilot --skill tanstack-form-composition --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.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/tanstack-form-composition

Context preview

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

Migrate a React @tanstack/react-form codebase from the prop-drilled `useForm` + erased-form-type pattern to the official `createFormHook` composition API (`useAppForm` / `withForm` / `field.X`). Use when a project threads a `form` object (often cast to an `any`-erased type like

SKILL.md

tanstack-form-composition.SKILL.md
name: tanstack-form-composition
description: >-
  Migrate a React @tanstack/react-form codebase from the prop-drilled
  `useForm` + erased-form-type pattern to the official `createFormHook`
  composition API (`useAppForm` / `withForm` / `field.X`). Use when a project
  threads a `form` object (often cast to an `any`-erased type like
  `ReactFormExtendedApi<any,...>`) through field-wrapper components that take
  `form`+`name` props, and you want typed field names/values, no casts, and
  reusable bound field components. Triggers: "migrate forms to createFormHook",
  "adopt useAppForm/withForm", "remove AnyReactForm cast", "type-safe tanstack
  form fields".

TanStack Form → composition API migration

Convert a React project using `@tanstack/react-form` from **raw `useForm` + prop-drilled field wrappers + an `any`-erased form type** to the **`createFormHook` composition API**. The payoff: typed field names/values, deletion of the erased form type and every `as unknown as <ErasedForm>` cast, and bound field components consumed as `field.TextField` instead of `<FieldWrapper form={form} name=… />`.

This is for the **React** adapter. Solid/Vue/Angular have the same API shape with different hook names - the concepts below port directly.

When this applies

The codebase has most of these symptoms:

  • An erased form type alias, e.g. `type AnyForm = ReactFormExtendedApi<any,any,…>`

(12 `any` generics), used as a prop type and reached via `form as unknown as AnyForm`.

  • Field components that accept `form` + `name` props and internally render

`<form.Field name={name}>…</form.Field>`.

  • Shared "section" components that receive the whole `form` as a prop and are

reused across multiple parent forms (e.g. a settings form and an onboarding wizard).

  • Helper functions that take the `form` and call `getFieldValue`/`setFieldValue`.

If the project does **not** yet use `@tanstack/react-form`, stop - this skill migrates an existing TanStack Form codebase; it does not introduce the library.

Method

Work in four phases. Do recon fully before editing; build the shared infra once; then migrate call sites; then delete the erased type and verify. On a large repo, migrate one simple form end-to-end first as a reference, then fan out.

Phase 0 - Recon

Confirm the version and map the blast radius. Run (adapt paths):

  • Dependency: grep `package.json` for `@tanstack/react-form` (need v1+; `createFormHook`,

`createFormHookContexts`, `withForm` are stable in v1).

  • Form-creation sites: search `useForm(`.
  • The erased type + casts: search the alias name and `as unknown as`.
  • Existing field wrappers: the directory of components taking `form`/`name` props.
  • Shared sections: components whose props include the erased form type.
  • Form-consuming helpers: functions whose parameter is the erased form type.

Record every hit; these are your edit targets. Note which field wrappers are actually used (unused ones still get converted for library consistency, but have no call sites to update).

Phase 1 - Build the composition infra (once per form tree)

Create these alongside the existing field components (e.g. `components/ui/form/`).

**`form-context.ts`** - shared contexts, isolated to avoid a circular import with the hook:

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

export const { fieldContext, formContext, useFieldContext, useFormContext } =
  createFormHookContexts();

**`error-message.ts`** - one extractor (field `meta.errors` may hold Zod issue objects, strings, or thrown values):

export function firstErrorMessage(errors: ReadonlyArray<unknown>): string | undefined {
  const first = errors[0];
  if (!first) return undefined;
  if (typeof first === "string") return first;
  return (first as { message?: string }).message ?? String(first);
}

**Bound field components** - each reads `useFieldContext<T>()` instead of taking `form`/`name`. `useFieldContext<T>()` is a _local cast_: it does not enforce that `T` matches the actual field, so pick the widest value the component handles.

// thin wrapper over a presentational base component
export function TextField(props: TextFieldProps): ReactElement {
  const field = useFieldContext<string | number | null | undefined>();
  return (
    <BaseTextField
      value={field.state.value ?? ""}
      onChange={(e) => field.handleChange(/* coerce per type */ e.target.value)}
      onBlur={field.handleBlur}
      errorText={firstErrorMessage(field.state.meta.errors)}
      {...props}
    />
  );
}

**Form-level `SubmitButton`** - subscribes to submit state:

export function SubmitButton(props: SubmitButtonProps): ReactElement {
  const { children, disabled, ...rest } = props;
  const form = useFormContext();
  return (
    <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting] as const}>
      {([canSubmit, isSubmitting]) => (
        <Button type="submit" disabled={disabled || !canSubmit || isSubmitting} {...rest}>
          {children}
        </Button>
      )}
    </form.Subscribe>
  );
}

Keep `type="submit"` and let the enclosing `<form onSubmit>` drive submission; expose `disabled` so callers fold in external state (e.g. a pending mutation that the form's own `isSubmitting` doesn't observe).

**`index.ts`** - wire the hook. Name the bound components so the registry keys are shorthand:

import { createFormHook } from "@tanstack/react-form";
import { fieldContext, formContext } from "./form-context";

// import bound field components + SubmitButton

export const { useAppForm, withForm } = createFormHook({
  fieldContext,
  formContext,
  fieldComponents: { TextField, Select, Phone, Switch /* … */ },
  formComponents: { SubmitButton },
});

Phase 2 - Migrate call sites

**Form creation:** `useForm({ … })` → `useAppForm({ … })`. Same options object (`defaultValues`, `validators`, `onSubmit`). Delete the `const formApi = form as unknown as AnyForm` line.

**Fields:** prop-dr

Read more
Ships withjobpilot

An AI agent that applies to jobs for you, on the Claude or Codex subscription you already have.

Get the whole plugin

Other skills on jobpilot.