高效地大规模管理 React 表单
随着应用规模的扩大,在 React 中管理表单很容易变得棘手。这可能是由于您在启动项目时遵循的混乱方法,或者在每个组件中编写相同的样板文件,甚至有时为了应对棘手的用例而使用了一些 hack。说到解决方案,市面上有很多表单库,其中一些处理表单非常出色,但如何组织和保持代码整洁仍然取决于您。在本文中,我们将通过构建表单来解决这个问题,就像使用原生 HTML 编写表单一样简单,并且可以在任何生产环境中优雅地使用。
为了帮助您理解,我们将使用下面这段简单的代码来渲染表单。我们将使用React Hook Form,这是一个高性能且易于插件的 React 表单库。
<Form onSubmit={handleSubmit}>
<Input name="name" label="Name" />
<Input name="email" label="Email" type="email" />
<RadioGroup name="gender" options={radioOptions} />
<Dropdown name="location" options={dropdownOptions} />
<Checkbox name="terms" label="Accept terms of service" />
<Button>Submit</Button>
</Form>
在我们开始讨论代码之前,首先我想让您了解我们试图通过这种方法实现的目标。
- 减少样板代码——我们应该能够用干净且 DRY 的代码来呈现表单
- 验证——轻松执行简单和复杂的验证
- 灵活性——我们应该能够将输入字段放在表单内嵌套的任何级别
- 可访问性——在最基本的层面上,表单元素应该可以通过键盘轻松访问
- 自我检测提交按钮状态- 按钮应根据表单状态自动启用/禁用或显示加载器
- 性能——最后但并非最不重要的一点是,它至关重要,特别是在表单内呈现大量字段时。
设置 React 应用
考虑到上述要求,让我们开始通过运行来设置我们的 React 应用程序npx create-react-app react-form && yarn add react-hook-form
接下来,我们将创建可复用的表单和输入组件,以减少所有样板代码。React
Hook Form 提供了钩子useForm
,useFormContext
分别用于立即获取表单上下文以及在嵌套组件中获取表单上下文。我们将使用这两个钩子来实现表单和输入组件之间的通信。
首先,我们将创建Form
组件,然后创建输入组件,例如文本字段、复选框、单选按钮等。
构建表单组件
我们将使用钩子初始化表单useForm
,并通过组件将所有方法作为 props 传递给表单FormProvider
。这将为输入组件提供表单状态。
import React from 'react'
import { useForm, FormProvider } from 'react-hook-form'
export const Form = ({ initialValues, children, onSubmit }) => {
const methods = useForm({ defaultValues: initialValues })
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{children}
</form>
</FormProvider>
)
}
构建表单元素
现在我们已经Form
准备好组件了,我们将从组件开始创建表单元素Input
。
由于我们不知道输入组件的嵌套深度,我们将使用它来将其与之前创建的useFormContext
父组件连接起来。Form
import React from 'react'
import { useFormContext } from 'react-hook-form'
export const Input = ({ label, name }) => {
const { register } = useFormContext()
return (
<label>
{label}
<input name={name} ref={register} />
</label>
)
}
注意:使用
useFormContext
withFormContext
可能会因重新渲染而对性能造成一定影响,但如果您不在这些组件中进行任何昂贵的计算,则影响应该可以忽略不计。如果是这样,您可以使用 memo 来比较表单的脏状态。
复选框组件
我们将按照在组件中相同的方式创建并挂接该组件Input
,只需checkbox
为其添加类型即可。
import React from 'react'
import { useFormContext } from 'react-hook-form'
export const Checkbox = ({ label, name }) => {
const { register } = useFormContext()
return (
<label>
<input type="checkbox" name={name} ref={register} />
{label}
</label>
)
}
单选按钮组件
由于网页上没有单个单选按钮的用例,我们将创建一个RadioGroup
组件,该组件将接受字段数组并呈现一组单选按钮
import React from 'react'
import { useFormContext } from 'react-hook-form'
export const RadioGroup = ({ name, label, options }) => {
const { register } = useFormContext()
return (
<div>
<div>{label}</div>
{options && options.map(option => (
<label key={option.value}>
<input
type="radio"
name={name}
value={option.value}
ref={register}
/>
{option.label}
</label>
))}
</div>
)
}
下拉组件
这个组件与之前的组件略有不同,因为我们将使用第三方插件。考虑到可访问性,我发现最好的插件是Kent C Dodds 开发的downshift。这个库的另一个优点是它只提供下拉菜单的功能,我们也可以编写自己的 UI。
让我们安装插件yarn add downshift
并创建组件,如下所示:
import React, { useEffect } from 'react'
import { useFormContext } from 'react-hook-form'
import { useSelect } from 'downshift'
export const Dropdown = ({
name,
options,
label,
initialValue,
placeholder = 'Select...'
}) => {
const { register, setValue, getValues } = useFormContext()
const findInitialItem = () => {
const defaultValue = initialValue || getValues()[name]
if (defaultValue) {
return options.find(o => o.value === defaultValue)
}
}
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getItemProps,
} = useSelect({
items: options,
initialSelectedItem: findInitialItem()
})
useEffect(() => {
if (selectedItem) {
setValue(name, selectedItem.value);
}
}, [selectedItem, name, setValue]);
return (
<div>
<button type="button" {...getToggleButtonProps()}>
<label {...getLabelProps()}>{label}</label>
<input type="hidden" name={name} ref={register} />
<div>
{selectedItem ? selectedItem.text : placeholder}
</div>
</button>
<div {...getMenuProps()}>
{isOpen && (
options.map((item, index) => (
<div key={`${item.value}${index}`} {...getItemProps({ item, index })}>
{item.text}
</div>
))
)}
</div>
</div>
)
}
按钮组件
构建组件的目的Button
是让它自己处理启用/禁用/加载状态。在这里,我们只是禁用按钮,直到表单字段被清除或表单提交为止。
import React from 'react'
import { useFormContext } from 'react-hook-form'
export const Button = ({ children }) => {
const { formState: { isDirty, isSubmitting } } = useFormContext()
return (
<button type="submit" disabled={!isDirty || isSubmitting}>
{children}
</button>
)
}
验证表单
到目前为止,我们已经使表单具备了功能并能够提交表单数据。
接下来的需求是,我们需要为表单元素添加验证。我们将允许两种类型的验证:一种是简单地检查必填字段是否填写,另一种是通过提供一个模式来验证值。
例如,Input
我们之前创建的组件现在将接收两个额外的 props:required
和validation
。
import React from 'react'
import { useFormContext } from 'react-hook-form'
export const Input = ({ label, name, required, validation }) => {
const { register, errors } = useFormContext()
return (
<div>
<label>
{label}
<input
name={name}
ref={register(validation || { required: !!required })}
/>
</label>
{errors[name] && <i>{errors[name].message}</i>}
</div>
)
}
我们可以用同样的方式在其他组件中实现验证。
总结
在本文中,我们用最少的代码创建了组件。如果您想试用代码,请点击这里CodeSandbox 链接。
您还可以在 GitHub 上找到可立即使用的此代码的TypeScript 版本(此处有演示)。
文章来源:https://dev.to/prasanjit/managing-react-forms-efficiently-at-scale-194i