Tangramsangrams

TanStack Form

Generate formOptions for TanStack Form with schema validation.

TanStack Form

Tangrams generates formOptions with validation schemas from your mutations. These snap right into TanStack Form's useForm hook.

By default, Tangrams uses Zod for validation, but you can also use Valibot, ArkType, or Effect by setting the validator option in your config.

Peer Dependencies

Core

bun add @tanstack/react-form
npm install @tanstack/react-form
pnpm add @tanstack/react-form

Validation Library

Install your chosen validator (Zod is the default):

bun add zod
bun add valibot
bun add arktype
bun add effect

Setup

GraphQL Source

Configure a GraphQL source with "form" in the generates array:

import { defineConfig } from "tangrams"

export default defineConfig({
  sources: [
    {
      name: "graphql",
      type: "graphql",
      schema: {
        url: "http://localhost:4000/graphql",
      },
      documents: "./src/graphql/**/*.graphql",
      generates: ["form"],
    },
  ],
})

Forms are generated from mutation operations. Create mutations in your .graphql files:

# src/graphql/user.graphql

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
  }
}

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
  }
}

OpenAPI Source

Configure an OpenAPI source with "form" in the generates array:

import { defineConfig } from "tangrams"

export default defineConfig({
  sources: [
    {
      name: "api",
      type: "openapi",
      spec: "./openapi.yaml",
      generates: ["form"],
    },
  ],
})

Forms are generated from POST, PUT, and PATCH operations that have request bodies.

Generate

Run the generator:

bunx tangrams generate

Generated Output

Output Directory Structure

tangrams/
└── <source>/
    ├── schema.ts         # Validation schemas for request bodies
    └── form/
        └── options.ts    # formOptions exports

What Gets Generated

For each mutation with a request body, Tangrams generates a formOptions export with:

  • defaultValues - Empty object with type assertion for type safety
  • validators - The validation schema (configurable timing)

Example options.ts:

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

import { createUserRequestSchema, updateUserRequestSchema } from "../schema"
import type { CreateUserRequest, UpdateUserRequest } from "../schema"

export const createUserFormOptions = formOptions({
  defaultValues: {} as CreateUserRequest,
  validators: {
    onSubmitAsync: createUserRequestSchema,
  },
})

export const updateUserFormOptions = formOptions({
  defaultValues: {} as UpdateUserRequest,
  validators: {
    onSubmitAsync: updateUserRequestSchema,
  },
})

Usage

Basic Form

Spread the generated formOptions into useForm:

import { useForm } from "@tanstack/react-form"
import { createUserFormOptions } from "./tangrams/my-api/form/options"

function CreateUserForm() {
  const form = useForm({
    ...createUserFormOptions,
    onSubmit: async ({ value }) => {
      // value is fully typed
      console.log("Submitting:", value)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      <form.Field
        name="name"
        children={(field) => (
          <div>
            <label>Name</label>
            <input
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors.length > 0 && (
              <span>{field.state.meta.errors.join(", ")}</span>
            )}
          </div>
        )}
      />
      <form.Field
        name="email"
        children={(field) => (
          <div>
            <label>Email</label>
            <input
              type="email"
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors.length > 0 && (
              <span>{field.state.meta.errors.join(", ")}</span>
            )}
          </div>
        )}
      />
      <button type="submit">Create User</button>
    </form>
  )
}

Combining with Mutations

Use the generated formOptions alongside mutationOptions from TanStack Query for a complete form submission flow:

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

import { createUserFormOptions } from "./tangrams/my-api/form/options"
import { createUserMutationOptions, getUsersQueryOptions } from "./tangrams/my-api/query/options"

function CreateUserForm() {
  const mutation = useMutation({
    ...createUserMutationOptions(),
    onSuccess: (_d, _v, _r, { client }) => client.invalidateQueries(getUsersQueryOptions())
  })

  const form = useForm({
    ...createUserFormOptions,
    onSubmit: async ({ value }) => {
      await mutation.mutateAsync(value)
    },
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      {/* ... form fields */}
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? "Creating..." : "Create User"}
      </button>
      {mutation.isError && (
        <div>Error: {mutation.error.message}</div>
      )}
    </form>
  )
}

Setting Default Values

The generated formOptions use an empty object with a type assertion for defaultValues. You should provide your own default values when using the form:

import { useForm } from "@tanstack/react-form"
import { createUserFormOptions } from "./tangrams/my-api/form/options"
import type { CreateUserRequest } from "./tangrams/my-api/schema"

function CreateUserForm() {
  const defaultValues: CreateUserRequest = {
    name: "",
    email: "",
    role: "user"
  }

  const form = useForm({
    ...createUserFormOptions,
    defaultValues,
    onSubmit: async ({ value }) => {
      // ...
    },
  })

  // ...
}

Edit Forms with Existing Data

For edit forms, populate defaultValues with the existing data:

import { useForm } from "@tanstack/react-form"
import { updateUserFormOptions } from "./tangrams/my-api/form/options"
import type { UpdateUserRequest } from "./tangrams/my-api/schema"

function EditUserForm({ user }: { user: User }) {
  const defaultValues: UpdateUserRequest = {
    name: user.name,
    email: user.email,
    bio: user.bio ?? ""
  }

  const form = useForm({
    ...updateUserFormOptions,
    defaultValues,
    onSubmit: async ({ value }) => {
      // ...
    },
  })

  return (
    // ... form JSX
  )
}

Validator Configuration

By default, Tangrams generates form options with onSubmitAsync validation. You can customize the validator timing using the overrides.form configuration:

Available Validators

// tangrams.config.ts
import { defineConfig } from "tangrams"

export default defineConfig({
  sources: [
    {
      name: "api",
      type: "openapi",
      spec: "./openapi.yaml",
      generates: ["form"],
      overrides: {
        form: {
          // Choose one of:
          // "onChange"       - Validate on every change (sync)
          // "onChangeAsync"  - Validate on every change (async)
          // "onBlur"         - Validate when field loses focus (sync)
          // "onBlurAsync"    - Validate when field loses focus (async)
          // "onSubmit"       - Validate on submit (sync)
          // "onSubmitAsync"  - Validate on submit (async) - DEFAULT
          // "onDynamic"      - Dynamic validation with revalidation logic
          validator: "onBlurAsync",
        },
      },
    },
  ],
})

Dynamic Validation

The onDynamic validator uses TanStack Form's revalidateLogic for smart revalidation. Configure when validation runs initially and after the first submission:

overrides: {
  form: {
    validator: "onDynamic",
    validationLogic: {
      mode: "submit",                // Initial validation: "change" | "blur" | "submit"
      modeAfterSubmission: "change", // After first submit: "change" | "blur" | "submit"
    },
  },
}

Generated output with onDynamic:

import { formOptions, revalidateLogic } from "@tanstack/react-form"

import { createUserRequestSchema } from "../schema"
import type { CreateUserRequest } from "../schema"

export const createUserFormOptions = formOptions({
  defaultValues: {} as CreateUserRequest,
  validationLogic: revalidateLogic({ mode: "submit", modeAfterSubmission: "change" }),
  validators: {
    onDynamic: createUserRequestSchema,
  },
})

This pattern validates on submit initially, then revalidates on every change after the first submission attempt - a common UX pattern for forms.

Configuration Reference

GraphQL Source Options

OptionTypeRequiredDescription
namestringYesUnique name for this source (lowercase alphanumeric with hyphens, starting with a letter)
type"graphql"YesSource type
schemaobjectYesSchema configuration (URL or file-based)
documentsstring | string[]YesGlob pattern(s) for .graphql files
generatesarrayYesMust include "form"
overridesobjectNoOverride scalars, DB, and form settings (see Configuration Reference)

OpenAPI Source Options

OptionTypeRequiredDescription
namestringYesUnique name for this source (lowercase alphanumeric with hyphens, starting with a letter)
type"openapi"YesSource type
specstringYesPath to OpenAPI spec (local or URL)
headersRecord<string, string>NoHeaders for fetching remote spec
includestring[]NoGlob patterns for paths to include
excludestring[]NoGlob patterns for paths to exclude
generatesarrayYesMust include "form"
overridesobjectNoOverride DB and form settings (see Configuration Reference)

Form Overrides

Configure form generation behavior using overrides.form:

OptionTypeDefaultDescription
validatorstring"onSubmitAsync"Validator timing (see available validators)
validationLogicobject-Configuration for onDynamic validator only
validationLogic.modestring"submit"Initial validation trigger: "change", "blur", or "submit"
validationLogic.modeAfterSubmissionstring"change"Revalidation trigger after first submit

On this page