TanStack Form 与 React 开箱即用兼容,支持 SSR 并且与框架无关。但是,根据您选择的框架,需要进行特定的配置。
目前我们支持以下元框架
本节重点介绍如何将 TanStack Form 与 TanStack Start 集成。
让我们首先创建一个 formOption,我们将使用它在客户端和服务器之间共享表单的形状。
// app/routes/index.tsx, but can be extracted to any other path
import { formOptions } from '@tanstack/react-form'
// You can pass other form options here
export const formOpts = formOptions({
defaultValues: {
firstName: '',
age: 0,
},
})
// app/routes/index.tsx, but can be extracted to any other path
import { formOptions } from '@tanstack/react-form'
// You can pass other form options here
export const formOpts = formOptions({
defaultValues: {
firstName: '',
age: 0,
},
})
接下来,我们可以创建 一个 Start Server Action,它将处理服务器上的表单提交。
// 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',
})
.validator((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'
})
// 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',
})
.validator((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'
})
然后我们需要建立一种方法,使用另一个服务器 action 从 serverValidate 的 response 中获取表单数据
// 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()
},
)
// 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()
},
)
最后,我们将在 loader 中使用 getFormDataFromServer 从我们的服务器获取状态到我们的客户端,并在我们的客户端表单组件中使用 handleForm。
// app/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import {
mergeForm,
useForm,
useStore,
useTransform,
} from '@tanstack/react-form'
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 (
<form action={handleForm.url} method="post" encType={'multipart/form-data'}>
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 8 ? 'Client validation: You must be at least 8' : undefined,
}}
>
{(field) => {
return (
<div>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((error) => (
<p key={error as string}>{error}</p>
))}
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
// app/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import {
mergeForm,
useForm,
useStore,
useTransform,
} from '@tanstack/react-form'
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 (
<form action={handleForm.url} method="post" encType={'multipart/form-data'}>
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 8 ? 'Client validation: You must be at least 8' : undefined,
}}
>
{(field) => {
return (
<div>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((error) => (
<p key={error as string}>{error}</p>
))}
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
在阅读本节之前,建议您了解 React Server Components 和 React Server Actions 的工作原理。 查看此博客系列以获取更多信息
本节重点介绍如何将 TanStack Form 与 Next.js 集成,特别是使用 App Router 和 Server Actions。
让我们首先创建一个 formOption,我们将使用它在客户端和服务器之间共享表单的形状。
// 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,
},
})
// 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,
},
})
接下来,我们可以创建 一个 React Server Action,它将处理服务器上的表单提交。
// 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!
}
// 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!
}
最后,我们将在我们的客户端表单组件中使用 someAction。
// client-component.tsx
'use client'
import { useActionState } from 'react'
import { initialFormState } from '@tanstack/react-form/nextjs'
// Notice the import is from `react-form`, not `react-form/nextjs`
import {
mergeForm,
useForm,
useStore,
useTransform,
} from '@tanstack/react-form'
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 action={action as never} onSubmit={() => form.handleSubmit()}>
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 8 ? 'Client validation: You must be at least 8' : undefined,
}}
>
{(field) => {
return (
<div>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((error) => (
<p key={error as string}>{error}</p>
))}
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
// client-component.tsx
'use client'
import { useActionState } from 'react'
import { initialFormState } from '@tanstack/react-form/nextjs'
// Notice the import is from `react-form`, not `react-form/nextjs`
import {
mergeForm,
useForm,
useStore,
useTransform,
} from '@tanstack/react-form'
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 action={action as never} onSubmit={() => form.handleSubmit()}>
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 8 ? 'Client validation: You must be at least 8' : undefined,
}}
>
{(field) => {
return (
<div>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((error) => (
<p key={error as string}>{error}</p>
))}
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</form>
)
}
在这里,我们使用 React 的 useActionState hook 和 TanStack Form 的 useTransform hook 将从服务器 action 返回的状态与表单状态合并。
如果您在 Next.js 应用程序中收到以下错误
typescriptx 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.
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.
这是因为您没有从 @tanstack/react-form/nextjs 导入服务器端代码。请确保您根据环境导入正确的模块。
这是 Next.js 的一个限制。其他元框架可能不会有同样的问题。
在阅读本节之前,建议您了解 Remix actions 的工作原理。 查看 Remix 的文档以获取更多信息
让我们首先创建一个 formOption,我们将使用它在客户端和服务器之间共享表单的形状。
// 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,
},
})
// 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,
},
})
接下来,我们可以创建 一个 action,它将处理服务器上的表单提交。
// 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!
}
// 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!
}
最后,当表单提交时,action 将被调用。
// routes/_index/route.tsx
import { Form, useActionData } from '@remix-run/react'
import {
mergeForm,
useForm,
useStore,
useTransform,
} from '@tanstack/react-form'
import {
ServerValidateError,
createServerValidate,
formOptions,
initialFormState,
} 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<typeof action>()
const form = useForm({
...formOpts,
transform: useTransform(
(baseForm) => mergeForm(baseForm, actionData ?? initialFormState),
[actionData],
),
})
const formErrors = useStore(form.store, (formState) => formState.errors)
return (
<Form method="post" onSubmit={() => form.handleSubmit()}>
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 8 ? 'Client validation: You must be at least 8' : undefined,
}}
>
{(field) => {
return (
<div>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((error) => (
<p key={error as string}>{error}</p>
))}
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</Form>
)
}
// routes/_index/route.tsx
import { Form, useActionData } from '@remix-run/react'
import {
mergeForm,
useForm,
useStore,
useTransform,
} from '@tanstack/react-form'
import {
ServerValidateError,
createServerValidate,
formOptions,
initialFormState,
} 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<typeof action>()
const form = useForm({
...formOpts,
transform: useTransform(
(baseForm) => mergeForm(baseForm, actionData ?? initialFormState),
[actionData],
),
})
const formErrors = useStore(form.store, (formState) => formState.errors)
return (
<Form method="post" onSubmit={() => form.handleSubmit()}>
{formErrors.map((error) => (
<p key={error as string}>{error}</p>
))}
<form.Field
name="age"
validators={{
onChange: ({ value }) =>
value < 8 ? 'Client validation: You must be at least 8' : undefined,
}}
>
{(field) => {
return (
<div>
<input
name="age"
type="number"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((error) => (
<p key={error as string}>{error}</p>
))}
</div>
)
}}
</form.Field>
<form.Subscribe
selector={(formState) => [formState.canSubmit, formState.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
</form.Subscribe>
</Form>
)
}
在这里,我们使用 Remix 的 useActionData hook 和 TanStack Form 的 useTransform hook 将从服务器 action 返回的状态与表单状态合并。
您的每周 JavaScript 新闻速递。每周一免费发送给超过 100,000 名开发者。