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-formnpm install @tanstack/react-formpnpm add @tanstack/react-formValidation Library
Install your chosen validator (Zod is the default):
bun add zodbun add valibotbun add arktypebun add effectSetup
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 generateGenerated Output
Output Directory Structure
tangrams/
└── <source>/
├── schema.ts # Validation schemas for request bodies
└── form/
└── options.ts # formOptions exportsWhat Gets Generated
For each mutation with a request body, Tangrams generates a formOptions export with:
defaultValues- Empty object with type assertion for type safetyvalidators- 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
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name for this source (lowercase alphanumeric with hyphens, starting with a letter) |
type | "graphql" | Yes | Source type |
schema | object | Yes | Schema configuration (URL or file-based) |
documents | string | string[] | Yes | Glob pattern(s) for .graphql files |
generates | array | Yes | Must include "form" |
overrides | object | No | Override scalars, DB, and form settings (see Configuration Reference) |
OpenAPI Source Options
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique name for this source (lowercase alphanumeric with hyphens, starting with a letter) |
type | "openapi" | Yes | Source type |
spec | string | Yes | Path to OpenAPI spec (local or URL) |
headers | Record<string, string> | No | Headers for fetching remote spec |
include | string[] | No | Glob patterns for paths to include |
exclude | string[] | No | Glob patterns for paths to exclude |
generates | array | Yes | Must include "form" |
overrides | object | No | Override DB and form settings (see Configuration Reference) |
Form Overrides
Configure form generation behavior using overrides.form:
| Option | Type | Default | Description |
|---|---|---|---|
validator | string | "onSubmitAsync" | Validator timing (see available validators) |
validationLogic | object | - | Configuration for onDynamic validator only |
validationLogic.mode | string | "submit" | Initial validation trigger: "change", "blur", or "submit" |
validationLogic.modeAfterSubmission | string | "change" | Revalidation trigger after first submit |
