From f9616f9e5292bb14a8de050ce68c43fa24163f98 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Fri, 10 Apr 2026 22:02:08 +0200 Subject: [PATCH 1/5] docs(react-remix-form): fix useTransform import --- docs/framework/react/guides/ssr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md index 47c45ffb6..a39e4abce 100644 --- a/docs/framework/react/guides/ssr.md +++ b/docs/framework/react/guides/ssr.md @@ -415,13 +415,13 @@ import { useActionData, useForm, useStore, - useTransform, } from '@tanstack/react-form' import { ServerValidateError, createServerValidate, formOptions, initialFormState, + useTransform, } from '@tanstack/react-form-remix' import type { ActionFunctionArgs } from '@remix-run/node' From cdf783c50aaaf5e689a566c4b97edd9066243992 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Sat, 11 Apr 2026 10:09:04 +0200 Subject: [PATCH 2/5] docs(arrays): add array key docs --- docs/framework/react/guides/arrays.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/framework/react/guides/arrays.md b/docs/framework/react/guides/arrays.md index 90a5e49f4..5ffe40a3e 100644 --- a/docs/framework/react/guides/arrays.md +++ b/docs/framework/react/guides/arrays.md @@ -52,6 +52,10 @@ Finally, you can use a subfield like so: ``` +## Why Index as Key? + +You may notice that these examples use `key={i}` the array index as the key prop. React's documentation generally advises _against_ using array indices as keys when items can be reordered or deleted. TanStack Form is an exception to this rule. Because field names in TanStack Form arrays are index-based, using the array index as `key` is required, it keeps React component instances, form store state, and field names in sync. + ## Full Example ```jsx From 0842cff17e2e236025bcd0d198db065d78be5f6c Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Sat, 11 Apr 2026 12:10:01 +0200 Subject: [PATCH 3/5] chore: update ssr --- docs/config.json | 16 +- docs/framework/react/guides/nextjs.md | 162 ++++++ docs/framework/react/guides/remix.md | 158 ++++++ docs/framework/react/guides/ssr.md | 491 ------------------ docs/framework/react/guides/tanstack-start.md | 174 +++++++ 5 files changed, 504 insertions(+), 497 deletions(-) create mode 100644 docs/framework/react/guides/nextjs.md create mode 100644 docs/framework/react/guides/remix.md delete mode 100644 docs/framework/react/guides/ssr.md create mode 100644 docs/framework/react/guides/tanstack-start.md diff --git a/docs/config.json b/docs/config.json index 6b289ada6..b62b94a5d 100644 --- a/docs/config.json +++ b/docs/config.json @@ -156,8 +156,16 @@ "to": "framework/react/guides/react-native" }, { - "label": "SSR/TanStack Start/Next.js", - "to": "framework/react/guides/ssr" + "label": "TanStack Start", + "to": "framework/react/guides/tanstack-start" + }, + { + "label": "Next.js", + "to": "framework/react/guides/nextjs" + }, + { + "label": "Remix", + "to": "framework/react/guides/remix" }, { "label": "Debugging", @@ -418,10 +426,6 @@ "label": "Functions / useForm", "to": "framework/react/reference/functions/useForm" }, - { - "label": "Functions / useTransform", - "to": "framework/react/reference/functions/useTransform" - }, { "label": "Types / FieldComponent", "to": "framework/react/reference/type-aliases/FieldComponent" diff --git a/docs/framework/react/guides/nextjs.md b/docs/framework/react/guides/nextjs.md new file mode 100644 index 000000000..e1bea4937 --- /dev/null +++ b/docs/framework/react/guides/nextjs.md @@ -0,0 +1,162 @@ +--- +id: ssr +title: TanStack Form - NextJs +--- + +## Using TanStack Form in a Next.js App Router + +> Before reading this section, it's suggested you understand how React Server Components and React Server Actions work. [Check out this blog series for more information](https://playfulprogramming.com/collections/react-beyond-the-render) + +This section focuses on integrating TanStack Form with `Next.js`, particularly using the `App Router` and `Server Actions`. + +### Next.js Prerequisites + +- Start a new `Next.js` project, following the steps in the [Next.js Documentation](https://nextjs.org/docs/getting-started/installation). +- Install `@tanstack/react-form-nextjs` +- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] + +## App Router integration + +Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. + +```ts shared-code.ts +import { formOptions } from '@tanstack/react-form-nextjs' + +// You can pass other form options here +export const formOpts = formOptions({ + defaultValues: { + firstName: '', + age: 0, + }, +}) +``` + +Next, we can create [a React Server Action](https://playfulprogramming.com/posts/what-are-react-server-components) that will handle the form submission on the server. + +```ts action.ts +'use server' + +import { + ServerValidateError, + createServerValidate, +} from '@tanstack/react-form-nextjs' + +import { formOpts } from './shared-code' + +// Create the server action that will infer the types of the form from `formOpts` +const serverValidate = createServerValidate({ + ...formOpts, + onServerValidate: ({ value }) => { + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' + } + }, +}) + +export default async function someAction(prev: unknown, formData: FormData) { + try { + const validatedData = await serverValidate(formData) + console.log('validatedData', validatedData) + // Persist the form data to the database + // await sql` + // INSERT INTO users (name, email, password) + // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) + // ` + } catch (e) { + if (e instanceof ServerValidateError) { + return e.formState + } + + // Some other error occurred while validating your form + throw e + } + + // Your form has successfully validated! +} +``` + +Finally, we'll use `someAction` in our client-side form component. + +```tsx client-component.tsx +'use client' + +import { useActionState } from 'react' +import { + initialFormState, + mergeForm, + useForm, + useStore, + useTransform, +} from '@tanstack/react-form-nextjs' + +import someAction from './action' +import { formOpts } from './shared-code' + +export const ClientComp = () => { + const [state, action] = useActionState(someAction, initialFormState) + + const form = useForm({ + ...formOpts, + transform: useTransform((baseForm) => mergeForm(baseForm, state!), [state]), + }) + + const formErrors = useStore(form.store, (formState) => formState.errors) + + return ( +
form.handleSubmit()}> + {formErrors.map((error) => ( +

{error}

+ ))} + + + value < 8 ? 'Client validation: You must be at least 8' : undefined, + }} + > + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors.map((error) => ( +

{error}

+ ))} +
+ ) + }} +
+ [formState.canSubmit, formState.isSubmitting]} + > + {([canSubmit, isSubmitting]) => ( + + )} + +
+ ) +} +``` + +### useTransform + +you may have noticed util function `useTransform` being used throughout these examples, it's primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state. + +## debugging + +> If you get the following error in your Next.js application: +> +> ```typescript +> x You're importing a component that needs `useState`. This React hook only works in a client component. To fix, mark the file (or its parent) with the `"use client"` directive. +> ``` +> +> This is because you're not importing server-side code from `@tanstack/react-form-nextjs`. Ensure you're importing the correct module based on the environment. +> +> [This is a limitation of Next.js](https://github.com/phryneas/rehackt). Other meta-frameworks will likely not have this same problem. diff --git a/docs/framework/react/guides/remix.md b/docs/framework/react/guides/remix.md new file mode 100644 index 000000000..725391f7c --- /dev/null +++ b/docs/framework/react/guides/remix.md @@ -0,0 +1,158 @@ +--- +id: ssr +title: TanStack Form - Remix +--- + +## Using TanStack Form in Remix + +> Before reading this section, it's suggested you understand how Remix actions work. [Check out Remix's docs for more information](https://remix.run/docs/en/main/discussion/data-flow#route-action) + +### Remix Prerequisites + +- Start a new `Remix` project, following the steps in the [Remix Documentation](https://remix.run/docs/en/main/start/quickstart). +- Install `@tanstack/react-form` +- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] + +## Remix integration + +Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. + +```tsx routes/_index/route.tsx +import { formOptions } from '@tanstack/react-form-remix' + +// You can pass other form options here +export const formOpts = formOptions({ + defaultValues: { + firstName: '', + age: 0, + }, +}) +``` + +Next, we can create [an action](https://remix.run/docs/en/main/discussion/data-flow#route-action) that will handle the form submission on the server. + +```tsx routes/_index/route.tsx +import { + ServerValidateError, + createServerValidate, + formOptions, +} from '@tanstack/react-form-remix' + +import type { ActionFunctionArgs } from '@remix-run/node' + +// Create the server action that will infer the types of the form from `formOpts` +const serverValidate = createServerValidate({ + ...formOpts, + onServerValidate: ({ value }) => { + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' + } + }, +}) + +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData() + try { + const validatedData = await serverValidate(formData) + console.log('validatedData', validatedData) + // Persist the form data to the database + // await sql` + // INSERT INTO users (name, email, password) + // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) + // ` + } catch (e) { + if (e instanceof ServerValidateError) { + return e.formState + } + + // Some other error occurred while validating your form + throw e + } + + // Your form has successfully validated! +} +``` + +Finally, the `action` will be called when the form submits. + +```tsx +// routes/_index/route.tsx +import { Form, useActionData } from '@remix-run/react' +import { mergeForm, useForm, useStore } from '@tanstack/react-form' +import { + ServerValidateError, + createServerValidate, + formOptions, + initialFormState, + useTransform, +} from '@tanstack/react-form-remix' + +export default function Index() { + const actionData = useActionData() + + const form = useForm({ + ...formOpts, + transform: useTransform( + (baseForm) => mergeForm(baseForm, actionData ?? initialFormState), + [actionData], + ), + }) + + const formErrors = useStore(form.store, (formState) => formState.errors) + + return ( +
form.handleSubmit()}> + {formErrors.map((error) => ( +

{error}

+ ))} + + + value < 8 ? 'Client validation: You must be at least 8' : undefined, + }} + > + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors.map((error) => ( +

{error}

+ ))} +
+ ) + }} +
+ [formState.canSubmit, formState.isSubmitting]} + > + {([canSubmit, isSubmitting]) => ( + + )} + +
+ ) +} +``` + +### useTransform + +you may have noticed util function `useTransform` being used throughout these examples, it's primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state. + +```tsx +const form = useForm({ + ...formOpts, + transform: useTransform( + (baseForm) => mergeForm(baseForm, actionData ?? initialFormState), + [actionData], + ), +}) +``` diff --git a/docs/framework/react/guides/ssr.md b/docs/framework/react/guides/ssr.md deleted file mode 100644 index a39e4abce..000000000 --- a/docs/framework/react/guides/ssr.md +++ /dev/null @@ -1,491 +0,0 @@ ---- -id: ssr -title: React Meta-Framework Usage ---- - -TanStack Form is compatible with React out of the box, supporting `SSR` and being framework-agnostic. However, specific configurations are necessary, according to your chosen framework. - -Today we support the following meta-frameworks: - -- [TanStack Start](https://tanstack.com/start/) -- [Next.js](https://nextjs.org/) -- [Remix](https://remix.run) - -## Using TanStack Form in TanStack Start - -This section focuses on integrating TanStack Form with TanStack Start. - -### TanStack Start Prerequisites - -- Start a new `TanStack Start` project, following the steps in the [TanStack Start Quickstart Guide](https://tanstack.com/router/latest/docs/framework/react/guide/tanstack-start) -- Install `@tanstack/react-form` - -### Start integration - -Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. - -```typescript -// app/routes/index.tsx, but can be extracted to any other path -import { formOptions } from '@tanstack/react-form-start' - -// You can pass other form options here -export const formOpts = formOptions({ - defaultValues: { - firstName: '', - age: 0, - }, -}) -``` - -Next, we can create [a Start Server Function](https://tanstack.com/start/latest/docs/framework/react/server-functions) that will handle the form submission on the server. - -```typescript -// app/routes/index.tsx, but can be extracted to any other path -import { - createServerValidate, - ServerValidateError, -} from '@tanstack/react-form-start' - -const serverValidate = createServerValidate({ - ...formOpts, - onServerValidate: ({ value }) => { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } - }, -}) - -export const handleForm = createServerFn({ - method: 'POST', -}) - .inputValidator((data: unknown) => { - if (!(data instanceof FormData)) { - throw new Error('Invalid form data') - } - return data - }) - .handler(async (ctx) => { - try { - const validatedData = await serverValidate(ctx.data) - console.log('validatedData', validatedData) - // Persist the form data to the database - // await sql` - // INSERT INTO users (name, email, password) - // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) - // ` - } catch (e) { - if (e instanceof ServerValidateError) { - // Log form errors or do any other logic here - return e.response - } - - // Some other error occurred when parsing the form - console.error(e) - setResponseStatus(500) - return 'There was an internal error' - } - - return 'Form has validated successfully' - }) -``` - -Then we need to establish a way to grab the form data from `serverValidate`'s `response` using another server action: - -```typescript -// app/routes/index.tsx, but can be extracted to any other path -import { getFormData } from '@tanstack/react-form-start' - -export const getFormDataFromServer = createServerFn({ method: 'GET' }).handler( - async () => { - return getFormData() - }, -) -``` - -Finally, we'll use `getFormDataFromServer` in our loader to get the state from our server into our client and `handleForm` in our client-side form component. - -```tsx -// app/routes/index.tsx -import { createFileRoute } from '@tanstack/react-router' -import { - mergeForm, - useForm, - useStore, - useTransform, -} from '@tanstack/react-form-start' - -export const Route = createFileRoute('/')({ - component: Home, - loader: async () => ({ - state: await getFormDataFromServer(), - }), -}) - -function Home() { - const { state } = Route.useLoaderData() - const form = useForm({ - ...formOpts, - transform: useTransform((baseForm) => mergeForm(baseForm, state), [state]), - }) - - const formErrors = useStore(form.store, (formState) => formState.errors) - - return ( -
- {formErrors.map((error) => ( -

{error}

- ))} - - - value < 8 ? 'Client validation: You must be at least 8' : undefined, - }} - > - {(field) => { - return ( -
- field.handleChange(e.target.valueAsNumber)} - /> - {field.state.meta.errors.map((error) => ( -

{error}

- ))} -
- ) - }} -
- [formState.canSubmit, formState.isSubmitting]} - > - {([canSubmit, isSubmitting]) => ( - - )} - -
- ) -} -``` - -## Using TanStack Form in a Next.js App Router - -> Before reading this section, it's suggested you understand how React Server Components and React Server Actions work. [Check out this blog series for more information](https://playfulprogramming.com/collections/react-beyond-the-render) - -This section focuses on integrating TanStack Form with `Next.js`, particularly using the `App Router` and `Server Actions`. - -### Next.js Prerequisites - -- Start a new `Next.js` project, following the steps in the [Next.js Documentation](https://nextjs.org/docs/getting-started/installation). Ensure you select `yes` for `Would you like to use App Router?` during the setup to access all new features provided by Next.js. -- Install `@tanstack/react-form` -- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] - -## App Router integration - -Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. - -```typescript -// shared-code.ts -// Notice the import path is different from the client -import { formOptions } from '@tanstack/react-form-nextjs' - -// You can pass other form options here -export const formOpts = formOptions({ - defaultValues: { - firstName: '', - age: 0, - }, -}) -``` - -Next, we can create [a React Server Action](https://playfulprogramming.com/posts/what-are-react-server-components) that will handle the form submission on the server. - -```typescript -// action.ts -'use server' - -// Notice the import path is different from the client -import { - ServerValidateError, - createServerValidate, -} from '@tanstack/react-form-nextjs' -import { formOpts } from './shared-code' - -// Create the server action that will infer the types of the form from `formOpts` -const serverValidate = createServerValidate({ - ...formOpts, - onServerValidate: ({ value }) => { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } - }, -}) - -export default async function someAction(prev: unknown, formData: FormData) { - try { - const validatedData = await serverValidate(formData) - console.log('validatedData', validatedData) - // Persist the form data to the database - // await sql` - // INSERT INTO users (name, email, password) - // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) - // ` - } catch (e) { - if (e instanceof ServerValidateError) { - return e.formState - } - - // Some other error occurred while validating your form - throw e - } - - // Your form has successfully validated! -} -``` - -Finally, we'll use `someAction` in our client-side form component. - -```tsx -// client-component.tsx -'use client' - -import { useActionState } from 'react' -import { - initialFormState, - mergeForm, - useForm, - useStore, - useTransform, -} from '@tanstack/react-form-nextjs' -import someAction from './action' -import { formOpts } from './shared-code' - -export const ClientComp = () => { - const [state, action] = useActionState(someAction, initialFormState) - - const form = useForm({ - ...formOpts, - transform: useTransform((baseForm) => mergeForm(baseForm, state!), [state]), - }) - - const formErrors = useStore(form.store, (formState) => formState.errors) - - return ( -
form.handleSubmit()}> - {formErrors.map((error) => ( -

{error}

- ))} - - - value < 8 ? 'Client validation: You must be at least 8' : undefined, - }} - > - {(field) => { - return ( -
- field.handleChange(e.target.valueAsNumber)} - /> - {field.state.meta.errors.map((error) => ( -

{error}

- ))} -
- ) - }} -
- [formState.canSubmit, formState.isSubmitting]} - > - {([canSubmit, isSubmitting]) => ( - - )} - -
- ) -} -``` - -Here, we're using [React's `useActionState` hook](https://playfulprogramming.com/posts/what-is-use-action-state-and-form-status) and TanStack Form's `useTransform` hook to merge state returned from the server action with the form state. - -> If you get the following error in your Next.js application: -> -> ```typescript -> x You're importing a component that needs `useState`. This React hook only works in a client component. To fix, mark the file (or its parent) with the `"use client"` directive. -> ``` -> -> This is because you're not importing server-side code from `@tanstack/react-form-nextjs`. Ensure you're importing the correct module based on the environment. -> -> [This is a limitation of Next.js](https://github.com/phryneas/rehackt). Other meta-frameworks will likely not have this same problem. - -## Using TanStack Form in Remix - -> Before reading this section, it's suggested you understand how Remix actions work. [Check out Remix's docs for more information](https://remix.run/docs/en/main/discussion/data-flow#route-action) - -### Remix Prerequisites - -- Start a new `Remix` project, following the steps in the [Remix Documentation](https://remix.run/docs/en/main/start/quickstart). -- Install `@tanstack/react-form` -- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] - -## Remix integration - -Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. - -```typescript -// routes/_index/route.tsx -import { formOptions } from '@tanstack/react-form-remix' - -// You can pass other form options here -export const formOpts = formOptions({ - defaultValues: { - firstName: '', - age: 0, - }, -}) -``` - -Next, we can create [an action](https://remix.run/docs/en/main/discussion/data-flow#route-action) that will handle the form submission on the server. - -```tsx -// routes/_index/route.tsx - -import { - ServerValidateError, - createServerValidate, - formOptions, -} from '@tanstack/react-form-remix' - -import type { ActionFunctionArgs } from '@remix-run/node' - -// export const formOpts = formOptions({ - -// Create the server action that will infer the types of the form from `formOpts` -const serverValidate = createServerValidate({ - ...formOpts, - onServerValidate: ({ value }) => { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } - }, -}) - -export async function action({ request }: ActionFunctionArgs) { - const formData = await request.formData() - try { - const validatedData = await serverValidate(formData) - console.log('validatedData', validatedData) - // Persist the form data to the database - // await sql` - // INSERT INTO users (name, email, password) - // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) - // ` - } catch (e) { - if (e instanceof ServerValidateError) { - return e.formState - } - - // Some other error occurred while validating your form - throw e - } - - // Your form has successfully validated! -} -``` - -Finally, the `action` will be called when the form submits. - -```tsx -// routes/_index/route.tsx -import { - Form, - mergeForm, - useActionData, - useForm, - useStore, -} from '@tanstack/react-form' -import { - ServerValidateError, - createServerValidate, - formOptions, - initialFormState, - useTransform, -} from '@tanstack/react-form-remix' - -import type { ActionFunctionArgs } from '@remix-run/node' - -// export const formOpts = formOptions({ - -// const serverValidate = createServerValidate({ - -// export async function action({request}: ActionFunctionArgs) { - -export default function Index() { - const actionData = useActionData() - - const form = useForm({ - ...formOpts, - transform: useTransform( - (baseForm) => mergeForm(baseForm, actionData ?? initialFormState), - [actionData], - ), - }) - - const formErrors = useStore(form.store, (formState) => formState.errors) - - return ( -
form.handleSubmit()}> - {formErrors.map((error) => ( -

{error}

- ))} - - - value < 8 ? 'Client validation: You must be at least 8' : undefined, - }} - > - {(field) => { - return ( -
- field.handleChange(e.target.valueAsNumber)} - /> - {field.state.meta.errors.map((error) => ( -

{error}

- ))} -
- ) - }} -
- [formState.canSubmit, formState.isSubmitting]} - > - {([canSubmit, isSubmitting]) => ( - - )} - -
- ) -} -``` - -Here, we're using [Remix's `useActionData` hook](https://remix.run/docs/en/main/hooks/use-action-data) and TanStack Form's `useTransform` hook to merge state returned from the server action with the form state. diff --git a/docs/framework/react/guides/tanstack-start.md b/docs/framework/react/guides/tanstack-start.md new file mode 100644 index 000000000..338ddac75 --- /dev/null +++ b/docs/framework/react/guides/tanstack-start.md @@ -0,0 +1,174 @@ +--- +id: ssr +title: TanStack Form - TanStack Start +--- + +## Using TanStack Form in TanStack Start + +> Before reading this section, it's suggested you understand how TanStack `serverFunctions`. [Check out TanStack docs for more information](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions) + +### TanStack Start Prerequisites + +- Start a new `TanStack Start` project, following the steps in the [TanStack Start Quickstart Guide](https://tanstack.com/router/latest/docs/framework/react/guide/tanstack-start) +- Install `@tanstack/react-form` +- Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] + +### Start integration + +Let's start by creating a `formOption` that we'll use to share the form's shape across the client and server. + +```tsx app/routes/index.tsx +import { formOptions } from '@tanstack/react-form-start' + +// You can pass other form options here +export const formOpts = formOptions({ + defaultValues: { + firstName: '', + age: 0, + }, +}) +``` + +Next, we can create [a Start Server Function](https://tanstack.com/start/latest/docs/framework/react/server-functions) that will handle the form submission on the server. + +```tsx app/routes/index.tsx +import { + createServerValidate, + ServerValidateError, +} from '@tanstack/react-form-start' + +const serverValidate = createServerValidate({ + ...formOpts, + onServerValidate: ({ value }) => { + if (value.age < 12) { + return 'Server validation: You must be at least 12 to sign up' + } + }, +}) + +export const handleForm = createServerFn({ + method: 'POST', +}) + .inputValidator((data: unknown) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid form data') + } + return data + }) + .handler(async (ctx) => { + try { + const validatedData = await serverValidate(ctx.data) + console.log('validatedData', validatedData) + // Persist the form data to the database + // await sql` + // INSERT INTO users (name, email, password) + // VALUES (${validatedData.name}, ${validatedData.email}, ${validatedData.password}) + // ` + } catch (e) { + if (e instanceof ServerValidateError) { + // Log form errors or do any other logic here + return e.response + } + + // Some other error occurred when parsing the form + console.error(e) + setResponseStatus(500) + return 'There was an internal error' + } + + return 'Form has validated successfully' + }) +``` + +Then we need to establish a way to grab the form data from `serverValidate`'s `response` using another server action: + +```tsx app/routes/index.tsx +import { getFormData } from '@tanstack/react-form-start' + +export const getFormDataFromServer = createServerFn({ method: 'GET' }).handler( + async () => { + return getFormData() + }, +) +``` + +Finally, we'll use `getFormDataFromServer` in our loader to get the state from our server into our client and `handleForm` in our client-side form component. + +```tsx app/routes/index.tsx +import { createFileRoute } from '@tanstack/react-router' +import { + mergeForm, + useForm, + useStore, + useTransform, +} from '@tanstack/react-form-start' + +export const Route = createFileRoute('/')({ + component: Home, + loader: async () => ({ + state: await getFormDataFromServer(), + }), +}) + +function Home() { + const { state } = Route.useLoaderData() + const form = useForm({ + ...formOpts, + transform: useTransform((baseForm) => mergeForm(baseForm, state), [state]), + }) + + const formErrors = useStore(form.store, (formState) => formState.errors) + + return ( +
+ {formErrors.map((error) => ( +

{error}

+ ))} + + + value < 8 ? 'Client validation: You must be at least 8' : undefined, + }} + > + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors.map((error) => ( +

{error}

+ ))} +
+ ) + }} +
+ [formState.canSubmit, formState.isSubmitting]} + > + {([canSubmit, isSubmitting]) => ( + + )} + +
+ ) +} +``` + +### useTransform + +you may have noticed util function `useTransform` being used throughout these examples, it's primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state. + +```tsx +const form = useForm({ + ...formOpts, + transform: useTransform((baseForm) => mergeForm(baseForm, state), [state]), +}) +``` From 707b5ed4b30f4cf1be20feafdc6bfff99c6ba285 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Sat, 11 Apr 2026 14:57:01 +0200 Subject: [PATCH 4/5] docs: add canSubmit invalid docs --- .../angular/guides/submission-handling.md | 19 ++++++++++++++++++ .../react/guides/submission-handling.md | 15 ++++++++++++++ .../solid/guides/submission-handling.md | 17 ++++++++++++++++ .../vue/guides/submission-handling.md | 20 +++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/docs/framework/angular/guides/submission-handling.md b/docs/framework/angular/guides/submission-handling.md index e5fae2477..696cd584f 100644 --- a/docs/framework/angular/guides/submission-handling.md +++ b/docs/framework/angular/guides/submission-handling.md @@ -102,3 +102,22 @@ export class AppComponent { }) } ``` + +> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. + +```angular-ts +import { injectForm } from '@tanstack/angular-form'; + +export class AppComponent { + form = injectForm({ + defaultValues: { + data: '', + }, + canSubmitWhenInvalid: true, + onSubmit: async ({ value }) => { + // Do something with the values + console.log(value) + }, + }); +} +``` diff --git a/docs/framework/react/guides/submission-handling.md b/docs/framework/react/guides/submission-handling.md index 7198217be..a66302a6a 100644 --- a/docs/framework/react/guides/submission-handling.md +++ b/docs/framework/react/guides/submission-handling.md @@ -91,3 +91,18 @@ const form = useForm({ }, }) ``` + +> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. + +```tsx +const form = useForm({ + defaultValues: { + data: '', + }, + canSubmitWhenInvalid: true, + onSubmit: async ({ value }) => { + // Do something with the values + console.log(value) + }, +}) +``` diff --git a/docs/framework/solid/guides/submission-handling.md b/docs/framework/solid/guides/submission-handling.md index 0ff523e1c..c072fbbda 100644 --- a/docs/framework/solid/guides/submission-handling.md +++ b/docs/framework/solid/guides/submission-handling.md @@ -96,3 +96,20 @@ const form = createForm(() => ({ }, })) ``` + +> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. + +```tsx +import { createForm } from '@tanstack/solid-form' + +const form = createForm(() => ({ + defaultValues: { + data: '', + }, + canSubmitWhenInvalid: true, + onSubmit: async ({ value }) => { + // Do something with the values + console.log(value) + }, +})) +``` diff --git a/docs/framework/vue/guides/submission-handling.md b/docs/framework/vue/guides/submission-handling.md index 2976e0b85..13352cc99 100644 --- a/docs/framework/vue/guides/submission-handling.md +++ b/docs/framework/vue/guides/submission-handling.md @@ -97,3 +97,23 @@ const form = useForm({ ``` + +> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. + +```vue + + +``` From aff13abf7f50f3ff568e722519dea548ca6b38e2 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Sat, 11 Apr 2026 15:06:32 +0200 Subject: [PATCH 5/5] chore: pr comments --- docs/framework/angular/guides/submission-handling.md | 2 +- docs/framework/react/guides/nextjs.md | 2 +- docs/framework/react/guides/remix.md | 6 +++--- docs/framework/react/guides/submission-handling.md | 2 +- docs/framework/react/guides/tanstack-start.md | 2 +- docs/framework/solid/guides/submission-handling.md | 2 +- docs/framework/vue/guides/submission-handling.md | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/framework/angular/guides/submission-handling.md b/docs/framework/angular/guides/submission-handling.md index 696cd584f..2623766ab 100644 --- a/docs/framework/angular/guides/submission-handling.md +++ b/docs/framework/angular/guides/submission-handling.md @@ -103,7 +103,7 @@ export class AppComponent { } ``` -> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. +> In a situation where you want to be able to submit in an invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. ```angular-ts import { injectForm } from '@tanstack/angular-form'; diff --git a/docs/framework/react/guides/nextjs.md b/docs/framework/react/guides/nextjs.md index e1bea4937..7215d1e2e 100644 --- a/docs/framework/react/guides/nextjs.md +++ b/docs/framework/react/guides/nextjs.md @@ -1,5 +1,5 @@ --- -id: ssr +id: nextjs title: TanStack Form - NextJs --- diff --git a/docs/framework/react/guides/remix.md b/docs/framework/react/guides/remix.md index 725391f7c..1f3b75989 100644 --- a/docs/framework/react/guides/remix.md +++ b/docs/framework/react/guides/remix.md @@ -1,5 +1,5 @@ --- -id: ssr +id: remix title: TanStack Form - Remix --- @@ -10,7 +10,7 @@ title: TanStack Form - Remix ### Remix Prerequisites - Start a new `Remix` project, following the steps in the [Remix Documentation](https://remix.run/docs/en/main/start/quickstart). -- Install `@tanstack/react-form` +- Install `@tanstack/react-form-start` - Install any [form validator](./validation#validation-through-schema-libraries) of your choice. [Optional] ## Remix integration @@ -145,7 +145,7 @@ export default function Index() { ### useTransform -you may have noticed util function `useTransform` being used throughout these examples, it's primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state. +you may have noticed util function `useTransform` being used throughout these examples, its primary responsibility is the merging of the server and client state. Under the hood it is a useCallback whose deps are that of the server state, when the server state changes it will automatically patch the client state. ```tsx const form = useForm({ diff --git a/docs/framework/react/guides/submission-handling.md b/docs/framework/react/guides/submission-handling.md index a66302a6a..341ab5f35 100644 --- a/docs/framework/react/guides/submission-handling.md +++ b/docs/framework/react/guides/submission-handling.md @@ -92,7 +92,7 @@ const form = useForm({ }) ``` -> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. +> In a situation where you want to be able to submit in an invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. ```tsx const form = useForm({ diff --git a/docs/framework/react/guides/tanstack-start.md b/docs/framework/react/guides/tanstack-start.md index 338ddac75..e4f513f71 100644 --- a/docs/framework/react/guides/tanstack-start.md +++ b/docs/framework/react/guides/tanstack-start.md @@ -1,5 +1,5 @@ --- -id: ssr +id: tanstack-start title: TanStack Form - TanStack Start --- diff --git a/docs/framework/solid/guides/submission-handling.md b/docs/framework/solid/guides/submission-handling.md index c072fbbda..25d095349 100644 --- a/docs/framework/solid/guides/submission-handling.md +++ b/docs/framework/solid/guides/submission-handling.md @@ -97,7 +97,7 @@ const form = createForm(() => ({ })) ``` -> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. +> In a situation where you want to be able to submit in an invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. ```tsx import { createForm } from '@tanstack/solid-form' diff --git a/docs/framework/vue/guides/submission-handling.md b/docs/framework/vue/guides/submission-handling.md index 13352cc99..224d1e2e4 100644 --- a/docs/framework/vue/guides/submission-handling.md +++ b/docs/framework/vue/guides/submission-handling.md @@ -98,7 +98,7 @@ const form = useForm({ ``` -> In a situation where you want to be able to submit in a invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. +> In a situation where you want to be able to submit in an invalid state `canSubmitWhenInvalid` boolean flag can be provided to useForm. ```vue