@tanstack-isomorphic-form/react
v1.2.0
Published
React bindings for progressively enhanced isomorphic forms in TanStack Start.
Maintainers
Readme
@tanstack-isomorphic-form/react
Progressively enhanced forms for @tanstack/react-start.
Table of Contents
Installation
pnpm add @tanstack-isomorphic-form/reactExports
createIsomorphicFormredirectAfterActionFormActionPanicError
Example
The example below shows the full flow for a simple todo creation form in @tanstack/react-start.
1. Create the server action and form
// 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,
});2. Use the loader in a React Start route
// 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>
);
}Example Notes
zodis used for the schema authoring experience.actionFnshould be acreateServerFnin a React Start app.- The same form works as a regular HTML form without JavaScript and as an enhanced form when JavaScript is available.
- The schema contract is based on
StandardSchemaV1plusStandardJSONSchemaV1.
API
createIsomorphicForm(options)
Creates a form definition and returns:
loader: reads the request, runs validation and the action onPOST, and returns a typed form stateuseIsomorphicForm: turns loader data intoformPropsand reactiveformState
When the schema contains a binary file field, formProps automatically sets enctype to multipart/form-data; other forms use application/x-www-form-urlencoded.
redirectAfterAction(options)
Creates a form action response that redirects after completion while preserving the form action result shape. Its destination is validated at compile time against your app's globally registered TanStack Router.
return redirectAfterAction({ to: "/todos" });FormActionPanicError
Represents an unexpected exception thrown while running the form action or loader flow.
Options
schema: required form schemaactionFn: a React Start server function, typicallycreateServerFn({ method: "POST" }), that receives validateddatadefaultValues: optional initial valuesformDataOptions: optional parsing delimiters for nested keysreturnedValueSanitizer: optional function to normalize values stored informState
useIsomorphicForm(loaderData, options?)
Turns loader data into:
formProps: props to spread onto the<form>formState: reactive state representing the current form lifecycle
Hook options:
defaultValues: optional initial values for this hook instance; these seedformState.valuesand take precedence over defaults passed to the loader, which themselves take precedence over defaults configured oncreateIsomorphicFormonSuccess: optional callback invoked after a successful actiononError: optional callback invoked for form-level errors and unexpected failures
formState
formState.status can be:
idlependingsuccesserror
formState also includes:
values: the extracted form valuesresult: present after a successful actionerror.schema: schema validation issueserror.form: form-level action error or unexpected error
Form Field Naming
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
You can override these delimiters with formDataOptions.
Error Handling
There are two error channels:
- schema errors: validation failed before your action ran
- form errors: your action returned
ok: falseor threw unexpectedly
Unexpected thrown errors are wrapped as FormActionPanicError.
Repository
- Source: https://github.com/cahnory/tanstack-isomorphic-form/tree/main/libs/react
- Project documentation: https://github.com/cahnory/tanstack-isomorphic-form#readme
- Issues: https://github.com/cahnory/tanstack-isomorphic-form/issues
License
MIT
