对 TanStack Form 的一个常见批评是其开箱即用的冗长性。虽然这可以用于教育目的 - 帮助加强对我们 API 的理解 - 但在生产用例中并不理想。
因此,虽然 form.Field 实现了 TanStack Form 最强大和最灵活的用法,但我们提供了包装它的 API,使您的应用程序代码不那么冗长。
组合表单最强大的方法是创建自定义表单 Hook。这允许您创建一个针对您的应用程序需求量身定制的表单 Hook,包括预绑定的自定义 UI 组件等等。
最基本的是,createFormHook 是一个函数,它接受 fieldContext 和 formContext 并返回一个 useAppForm Hook。
这个未自定义的 useAppForm Hook 与 useForm 相同,但是当我们向 createFormHook 添加更多选项时,这种情况将很快改变。
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
// export useFieldContext for use in your custom components
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// We'll learn more about these options later
fieldComponents: {},
formComponents: {},
})
function App() {
const form = useAppForm({
// Supports all useForm options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <form.Field /> // ...
}
import { createFormHookContexts, createFormHook } from '@tanstack/react-form'
// export useFieldContext for use in your custom components
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts()
const { useAppForm } = createFormHook({
fieldContext,
formContext,
// We'll learn more about these options later
fieldComponents: {},
formComponents: {},
})
function App() {
const form = useAppForm({
// Supports all useForm options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <form.Field /> // ...
}
一旦这个脚手架就位,您就可以开始向您的表单 Hook 添加自定义字段和表单组件。
注意:useFieldContext 必须与您的自定义表单上下文中导出的那个相同
import { useFieldContext } from './form-context.tsx'
export function TextField({ label }: { label: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<label>
<div>{label}</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)
}
import { useFieldContext } from './form-context.tsx'
export function TextField({ label }: { label: string }) {
// The `Field` infers that it should have a `value` type of `string`
const field = useFieldContext<string>()
return (
<label>
<div>{label}</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)
}
然后您可以将此组件注册到您的表单 Hook。
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 (
// Notice the `AppField` instead of `Field`; `AppField` provides the required context
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
)
}
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return (
// Notice the `AppField` instead of `Field`; `AppField` provides the required context
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
)
}
这不仅允许您重用共享组件的 UI,而且保留了您期望从 TanStack Form 获得的类型安全:输入错误 name 并获得 TypeScript 错误。
虽然上下文是 React 生态系统中的一个有价值的工具,但许多用户都恰当地担心通过上下文提供响应式值会导致不必要的重新渲染。
不熟悉这种性能问题?Mark Erikson 的博客文章解释了为什么 Redux 解决了许多这些问题 是一个很好的起点。
虽然这是一个值得指出的好问题,但对于 TanStack Form 来说这不是问题;通过上下文提供的值本身不是响应式的,而是具有响应式属性的静态类实例(使用 TanStack Store 作为我们的信号实现来支持显示)。
虽然 form.AppField 解决了字段样板代码和可重用性方面的许多问题,但它没有解决表单样板代码和可重用性的问题。
特别是,能够为例如响应式表单提交按钮共享 form.Subscribe 实例是一个常见的用例。
function SubscribeButton({ label }: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => <button disabled={isSubmitting}>{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>
// Notice the `AppForm` component wrapper; `AppForm` provides the required
context
<form.SubscribeButton label="Submit" />
</form.AppForm>
)
}
function SubscribeButton({ label }: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => <button disabled={isSubmitting}>{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>
// Notice the `AppForm` component wrapper; `AppForm` provides the required
context
<form.SubscribeButton label="Submit" />
</form.AppForm>
)
}
有时表单变得非常大;有时就是这样。虽然 TanStack Form 很好地支持大型表单,但处理数百或数千行代码长的文件从来都不是一件有趣的事情。
为了解决这个问题,我们支持使用 withForm 高阶组件将表单拆分成更小的部分。
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
const ChildForm = withForm({
// These values are only used for type-checking, and are not used at runtime
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
// Optional, but adds props to the `render` function in addition to `form`
props: {
// These props are also set as default values for the `render` function
title: 'Child Form',
},
render: function Render({ form, title }) {
return (
<div>
<p>{title}</p>
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<form.AppForm>
<form.SubscribeButton label="Submit" />
</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({
// These values are only used for type-checking, and are not used at runtime
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
// Optional, but adds props to the `render` function in addition to `form`
props: {
// These props are also set as default values for the `render` function
title: 'Child Form',
},
render: function Render({ form, title }) {
return (
<div>
<p>{title}</p>
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<form.AppForm>
<form.SubscribeButton label="Submit" />
</form.AppForm>
</div>
)
},
})
function App() {
const form = useAppForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
return <ChildForm form={form} title={'Testing'} />
}
为什么是高阶组件而不是 Hook?
虽然 Hook 是 React 的未来,但高阶组件仍然是用于组合的强大工具。特别是,useForm 的 API 使我们能够在不需要用户传递泛型的情况下拥有强大的类型安全性。
为什么我在 render 中收到关于 Hook 的 ESLint 错误?
ESLint 在函数的顶层查找 Hook,并且 render 可能不被识别为顶层组件,这取决于您如何定义它。
// This will cause ESLint errors with hooks usage
const ChildForm = withForm({
// ...
render: ({ form, title }) => {
// ...
},
})
// This will cause ESLint errors with hooks usage
const ChildForm = withForm({
// ...
render: ({ form, title }) => {
// ...
},
})
// This works fine
const ChildForm = withForm({
// ...
render: function Render({ form, title }) {
// ...
},
})
// This works fine
const ChildForm = withForm({
// ...
render: function Render({ form, title }) {
// ...
},
})
虽然上面的示例非常适合入门,但它们对于某些用例并不理想,在这些用例中,您可能有数百个表单和字段组件。特别是,您可能不希望将所有表单和字段组件都包含在使用您的表单 Hook 的每个文件的捆绑包中。
为了解决这个问题,您可以将 createFormHook TanStack API 与 React lazy 和 Suspense 组件混合使用
// src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/react-form'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
// src/hooks/form-context.ts
import { createFormHookContexts } from '@tanstack/react-form'
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
// src/components/text-field.tsx
import { useFieldContext } from '../hooks/form-context.tsx'
export default function TextField({ label }: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{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({ label }: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{label}</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)
}
// src/hooks/form.ts
import { lazy } from 'react'
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 'react'
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 'react'
import { PeoplePage } from './features/people/page.tsx'
export default function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<PeopleForm />
</Suspense>
)
}
// src/App.tsx
import { Suspense } from 'react'
import { PeoplePage } from './features/people/page.tsx'
export default function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<PeopleForm />
</Suspense>
)
}
这将显示 Suspense 后备,同时加载 TextField 组件,然后在加载后渲染表单。
现在我们已经介绍了创建自定义表单 Hook 的基础知识,让我们在一个示例中将所有内容整合在一起。
// /src/hooks/form.ts, to be used across the entire app
const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
function TextField({ label }: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{label}</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)
}
function SubscribeButton({ label }: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => <button disabled={isSubmitting}>{label}</button>}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
// /src/features/people/shared-form.ts, to be used across `people` features
const formOpts = formOptions({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
// /src/features/people/nested-form.ts, to be used in the `people` page
const ChildForm = withForm({
...formOpts,
// Optional, but adds props to the `render` function outside of `form`
props: {
title: 'Child Form',
},
render: ({ form, title }) => {
return (
<div>
<p>{title}</p>
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<form.AppForm>
<form.SubscribeButton label="Submit" />
</form.AppForm>
</div>
)
},
})
// /src/features/people/page.ts
const Parent = () => {
const form = useAppForm({
...formOpts,
})
return <ChildForm form={form} title={'Testing'} />
}
// /src/hooks/form.ts, to be used across the entire app
const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts()
function TextField({ label }: { label: string }) {
const field = useFieldContext<string>()
return (
<label>
<div>{label}</div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
</label>
)
}
function SubscribeButton({ label }: { label: string }) {
const form = useFormContext()
return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => <button disabled={isSubmitting}>{label}</button>}
</form.Subscribe>
)
}
const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
},
formComponents: {
SubscribeButton,
},
fieldContext,
formContext,
})
// /src/features/people/shared-form.ts, to be used across `people` features
const formOpts = formOptions({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
},
})
// /src/features/people/nested-form.ts, to be used in the `people` page
const ChildForm = withForm({
...formOpts,
// Optional, but adds props to the `render` function outside of `form`
props: {
title: 'Child Form',
},
render: ({ form, title }) => {
return (
<div>
<p>{title}</p>
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<form.AppForm>
<form.SubscribeButton label="Submit" />
</form.AppForm>
</div>
)
},
})
// /src/features/people/page.ts
const Parent = () => {
const form = useAppForm({
...formOpts,
})
return <ChildForm form={form} title={'Testing'} />
}
这是一个图表,可帮助您决定应该使用哪些 API
您的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发人员。