TanStack Form 的一个常见批评是其开箱即用的冗长性。虽然这_可能_对教育目的有用 - 有助于强化对我们 API 的理解 - 但在生产用例中并不理想。
因此,虽然 form.Field 能够实现 TanStack Form 最强大和灵活的使用,但我们提供了包装它的 API,使您的应用程序代码不那么冗长。
组合表单最强大的方法是创建自定义表单钩子。这允许您创建一个针对应用程序需求量身定制的表单钩子,包括预绑定的自定义 UI 组件等。
在最基本的情况下,createFormHook 是一个接受 fieldContext 和 formContext 并返回 useAppForm 钩子的函数。
这个未自定义的 useAppForm 钩子与 useForm 相同,但随着我们向 createFormHook 添加更多选项,这将很快改变。
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
// 导出 useFieldContext 以在您的自定义组件中使用
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// 我们稍后会了解更多关于这些选项的信息
fieldComponents: {},
formComponents: {},
})
function App() {
const form = useAppForm({
// 支持所有 useForm 选项
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <form.Field /> // ...
}
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
// 导出 useFieldContext 以在您的自定义组件中使用
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// 我们稍后会了解更多关于这些选项的信息
fieldComponents: {},
formComponents: {},
})
function App() {
const form = useAppForm({
// 支持所有 useForm 选项
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <form.Field /> // ...
}
一旦这个脚手架就位,您就可以开始向表单钩子添加自定义字段和表单组件。
注意:useFieldContext 必须是从您的自定义表单上下文导出的同一个
import { useFieldContext } from './form-context.tsx'
export function TextField(props: { label: string }) {
// `Field` 推断它应该具有 `string` 类型的 `value`
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
import { useFieldContext } from './form-context.tsx'
export function TextField(props: { label: string }) {
// `Field` 推断它应该具有 `string` 类型的 `value`
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
然后您可以将此组件注册到您的表单钩子中。
import { TextField } from './text-field.tsx'
const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
import { TextField } from './text-field.tsx'
const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
并在您的表单中使用它:
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return (
// 注意使用 `AppField` 而不是 `Field`;`AppField` 提供所需的上下文
<form.AppField
name="firstName"
children={(field) => <field.TextField label="名字" />}
/>
)
}
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return (
// 注意使用 `AppField` 而不是 `Field`;`AppField` 提供所需的上下文
<form.AppField
name="firstName"
children={(field) => <field.TextField label="名字" />}
/>
)
}
这不仅允许您重用共享组件的 UI,还保留了您从 TanStack Form 期望的类型安全性:拼写错误的 name 会得到 TypeScript 错误。
虽然 form.AppField 解决了字段样板代码和可重用性的许多问题,但它没有解决_表单_样板代码和可重用性��问题。
特别是,能够共享 form.Subscribe 实例,比如用于响应式表单提交按钮,这是一个常见的用例。
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting()}>
{props.label}
</button>
)}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return (
<form.AppForm>
// 注意 `AppForm` 组件包装器;`AppForm` 提供所需的上下文
<form.SubscribeButton label="提交" />
</form.AppForm>
)
}
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting()}>
{props.label}
</button>
)}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return (
<form.AppForm>
// 注意 `AppForm` 组件包装器;`AppForm` 提供所需的上下文
<form.SubscribeButton label="提交" />
</form.AppForm>
)
}
有时表单会变得非常大;有时就是这样。虽然 TanStack Form 很好地支持大型表单,但处理数百或数千行代码的长文件从来都不是一件有趣的事。
为了解决这个问题,我们支持使用 withForm 高阶组件将表单分解为更小的部分。
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
const ChildForm = withForm({
// 这些值仅用于类型检查,在运行时不使用
// 这允许您从 `formOptions` 中使用 `...formOpts` 而无需重新声明选项
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
// 可选,但除了 `form` 之外还向 `render` 函数添加属性
props: {
// 这些属性也被设置为 `render` 函数的默认值
title: 'Child Form',
},
render: function Render(props) {
return (
<div>
<p>{props.title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="名字" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="提交" />
</props.form.AppForm>
</div>
)
},
})
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <ChildForm form={form} title={'Testing'} />
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
const ChildForm = withForm({
// 这些值仅用于类型检查,在运行时不使用
// 这允许您从 `formOptions` 中使用 `...formOpts` 而无需重新声明选项
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
// 可选,但除了 `form` 之外还向 `render` 函数添加属性
props: {
// 这些属性也被设置为 `render` 函数的默认值
title: 'Child Form',
},
render: function Render(props) {
return (
<div>
<p>{props.title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="名字" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="提交" />
</props.form.AppForm>
</div>
)
},
})
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <ChildForm form={form} title={'Testing'} />
}
为什么使用高阶组件而不是钩子?
虽然钩子�� React 的未来,但高阶组件仍然是组合的强大工具。特别是,withForm 的 API 使我们能够拥有强类型安全性,而无需要求用户传递泛型。
虽然上面的示例非常适合入门,但对于某些可能有数百个表单和字段组件的用例来说,它们并不理想。 特别是,您可能不希望在使用表单钩子的每个文件的包中包含所有表单和字段组件。
为了解决这个问题,您可以将 createFormHook TanStack API 与 Solid 的 lazy 和 Suspense 组件混合使用:
// src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/solid-form'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
// src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/solid-form'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
// src/components/text-field.tsx
import { useFieldContext } from '../hooks/form-context.tsx'
export default function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
// src/components/text-field.tsx
import { useFieldContext } from '../hooks/form-context.tsx'
export default function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
// src/hooks/form.ts
import { lazy } from 'solid-js'
import { createFormHook } from '@tanstack/react-form'
const TextField = lazy(() => import('../components/text-fields.tsx'))
const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
// src/hooks/form.ts
import { lazy } from 'solid-js'
import { createFormHook } from '@tanstack/react-form'
const TextField = lazy(() => import('../components/text-fields.tsx'))
const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
},
formComponents: {},
})
// src/App.tsx
import { Suspense } from 'solid-js'
import { PeoplePage } from './features/people/form.tsx'
export default function App() {
return (
<Suspense fallback={<p>加载中...</p>}>
<PeoplePage />
</Suspense>
)
}
// src/App.tsx
import { Suspense } from 'solid-js'
import { PeoplePage } from './features/people/form.tsx'
export default function App() {
return (
<Suspense fallback={<p>加载中...</p>}>
<PeoplePage />
</Suspense>
)
}
这将在加载 TextField 组件时显示 Suspense 回退,然后在加载完成后渲染表单。
现在我们已经介绍了创建自定义表单钩子的基础知识,让我们在一个示例中将所有内容整合在一起。
// /src/hooks/form.ts,在整个应用程序中使用
const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button disabled={isSubmitting()}>{props.label}</button>
)}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
// /src/features/people/shared-form.ts,在 `people` 功能中使用
const formOpts = formOptions({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
// /src/features/people/nested-form.ts,在 `people` 页面中使用
const ChildForm = withForm({
...formOpts,
// 可选,但在 `form` 之外向 `render` 函数添加属性
props: {
title: 'Child Form',
},
render: (props) => {
return (
<div>
<p>{title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="名字" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="提交" />
</props.form.AppForm>
</div>
)
},
})
// /src/features/people/page.ts
const Parent = () => {
const form = useAppForm({
...formOpts,
})
return <ChildForm form={form} title={'Testing'} />
}
// /src/hooks/form.ts,在整个应用程序中使用
const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
function TextField(props: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{props.label}</div>
<input
value={field().state.value}
onChange={(e) => field().handleChange(e.target.value)}
/>
</label>
)
}
function SubscribeButton(props: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button disabled={isSubmitting()}>{props.label}</button>
)}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
// /src/features/people/shared-form.ts,在 `people` 功能中使用
const formOpts = formOptions({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
// /src/features/people/nested-form.ts,在 `people` 页面中使用
const ChildForm = withForm({
...formOpts,
// 可选,但在 `form` 之外向 `render` 函数添加属性
props: {
title: 'Child Form',
},
render: (props) => {
return (
<div>
<p>{title}</p>
<props.form.AppField
name="firstName"
children={(field) => <field.TextField label="名字" />}
/>
<props.form.AppForm>
<props.form.SubscribeButton label="提交" />
</props.form.AppForm>
</div>
)
},
})
// /src/features/people/page.ts
const Parent = () => {
const form = useAppForm({
...formOpts,
})
return <ChildForm form={form} title={'Testing'} />
}
这里有一个图表来帮助您决定应该使用哪些 API: