Progressively enhanced forms for TanStack Start.
This library helps you build forms that work:
- without JavaScript, through normal browser form submission
- with JavaScript, with a smoother client-side submit experience
- with the same validation and action logic in both cases
It is built around an isomorphic form model: keep one form flow, one validation story, and one action contract across server-rendered and client-enhanced usage.
Today, the available package is:
- Features
- Packages
- Installation
- Current Example: React Start
- Shared API
- Package Documentation
- Contributing
- Status
- License
- Works with or without JavaScript enabled
- One form flow for server submission and client enhancement
- Shared form contract across framework-specific packages
- Typed values inferred from your schema
- Automatic multipart encoding for forms with binary file fields
- Schema errors and form-level errors exposed separately
- Redirect support after submit
@tanstack-isomorphic-form/react: React integration for TanStack Start- future framework-specific packages are expected to expose the same shared contract
pnpm add @tanstack-isomorphic-form/reactThe example below uses @tanstack-isomorphic-form/react, which is the current framework package. Future framework packages should follow the same shared contract while exposing their own integration-specific helpers.
// src/features/todos/todos.form.ts
import { createServerFn } from "@tanstack/react-start";
import { createIsomorphicForm } from "@tanstack-isomorphic-form/react";
import { z } from "zod";
import { createTodo } from "./todos.service.ts";
const todoSchema = z.object({
title: z.string().trim().min(1, "Title is required").max(120, "Title is too long"),
});
const createTodoAction = createServerFn({ method: "POST" })
.validator(todoSchema)
.handler(async ({ data }) => {
try {
return {
ok: true,
result: await createTodo(data),
};
} catch {
return {
ok: false,
error: "Unable to save the todo. Retry later.",
};
}
});
export const createTodoForm = createIsomorphicForm({
schema: todoSchema,
actionFn: createTodoAction,
});// src/routes/todos.tsx
import { createFileRoute } from "@tanstack/react-router";
import { createTodoForm } from "../features/todos/todos.form.ts";
export const Route = createFileRoute("/todos")({
loader: async (loaderOptions) => ({
formLoaderData: await createTodoForm.loader(loaderOptions),
}),
component: CreateTodoPage,
});
function CreateTodoPage() {
const { formLoaderData } = Route.useLoaderData();
const { formProps, formState } = createTodoForm.useIsomorphicForm(formLoaderData);
const titleError =
formState.error?.schema?.find((issue) => issue.path?.[0] === "title")?.message ?? null;
return (
<main>
<h1>Add a todo</h1>
<form {...formProps}>
<label htmlFor="title">Title</label>
<input
id="title"
name="title"
defaultValue={formState.values.title ?? ""}
placeholder="Buy milk"
/>
{titleError ? <p>{titleError}</p> : null}
{formState.status === "error" && formState.error?.form ? (
<p>{String(formState.error.form)}</p>
) : null}
{formState.status === "success" ? <p>Todo created successfully.</p> : null}
<button type="submit" disabled={formState.status === "pending"}>
{formState.status === "pending" ? "Saving..." : "Add todo"}
</button>
</form>
</main>
);
}zodis used here for schema authoring.- In a React Start app,
actionFnshould be acreateServerFn. - The same form works as a regular HTML form without JavaScript and as an enhanced form when JavaScript is available.
- The React-specific hook shown here is package-specific, while the loader/action contract is intended to stay shared.
Creates a form definition around a shared cross-package contract.
Across framework packages, it returns:
loader: reads the request, runs validation and the action onPOST, and returns a typed form state
Framework packages can also return integration-specific helpers.
For example:
@tanstack-isomorphic-form/reactalso returnsuseIsomorphicForm
Helper for returning a redirect from your action while preserving the form action result shape.
When imported from @tanstack-isomorphic-form/react, the destination is validated
against your app's registered TanStack Router at compile time.
Represents an unexpected exception thrown while running the form action or loader flow.
schema: required form schemaactionFn: server action used by the framework integrationdefaultValues: optional initial valuesformDataOptions: optional parsing delimiters for nested keysreturnedValueSanitizer: optional function to normalize values stored in form state
Some framework packages may add more options when needed, while the shared options stay aligned.
The shared loader result is designed around these states:
idlependingsuccesserror
The form state includes:
values: the extracted form valuesresult: present after a successful actionerror.schema: schema validation issueserror.form: form-level action error or unexpected error
Nested objects and arrays are extracted from FormData key names. By default, these delimiters are used:
- object properties:
. - array indexes:
[and]
Examples:
titleaddress.streetitems[0].label
Unexpected thrown errors are wrapped as FormActionPanicError.
- React package guide:
libs/react/README.md - Core package overview:
libs/core/README.md
Contributor workflows and release expectations are documented below.
pnpm install
pnpm check
pnpm fixThis repository uses Changesets for versioning and npm publication.
For any package-affecting change:
pnpm changesetChoose the correct bump type:
patchfor fixesminorfor backward-compatible featuresmajorfor breaking changes
The CI workflow then enforces:
pnpm checkpassespnpm buildpasses- both packages keep the same version number
Pull requests should use the Changesets GitHub app for changeset guidance:
- install
changeset-boton the repository - the bot comments when a PR may need a changeset
- if the PR affects a release, run
pnpm changeset
On every push to main, GitHub Actions runs Changesets:
- if unreleased changesets exist, it opens or updates a release PR
- merging that PR creates the git tag and publishes both packages to npm
Repository setup still required:
- configure a GitHub Actions environment named
releaseand add the desired protection rules - configure npm trusted publishing for each package with the
release.ymlGitHub Actions workflow and thereleaseenvironment name - install the
changeset-botGitHub app on this repository - allow GitHub Actions to create pull requests if your repository settings restrict that
This repository is still early-stage. Expect the public API and packaging details to evolve.
MIT