高效地大规模管理 React 表单

2025-06-07

高效地大规模管理 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>
Enter fullscreen mode Exit fullscreen mode

在我们开始讨论代码之前,首先我想让您了解我们试图通过这种方法实现的目标。

  • 减少样板代码——我们应该能够用干净且 DRY 的代码来呈现表单
  • 验证——轻松执行简单和复杂的验证
  • 灵活性——我们应该能够将输入字段放在表单内嵌套的任何级别
  • 可访问性——在最基本的层面上,表单元素应该可以通过键盘轻松访问
  • 自我检测提交按钮状态- 按钮应根据表单状态自动启用/禁用或显示加载器
  • 性能——最后但并非最不重要的一点是,它至关重要,特别是在表单内呈现大量字段时。

设置 React 应用

考虑到上述要求,让我们开始通过运行来设置我们的 React 应用程序npx create-react-app react-form && yarn add react-hook-form

接下来,我们将创建可复用的表单和输入组件,以减少所有样板代码。React
Hook Form 提供了钩子useFormuseFormContext分别用于立即获取表单上下文以及在嵌套组件中获取表单上下文。我们将使用这两个钩子来实现表单和输入组件之间的通信。
首先,我们将创建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>
  )
}
Enter fullscreen mode Exit fullscreen mode

构建表单元素

现在我们已经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>
  )
}
Enter fullscreen mode Exit fullscreen mode

注意:使用useFormContextwithFormContext可能会因重新渲染而对性能造成一定影响,但如果您不在这些组件中进行任何昂贵的计算,则影响应该可以忽略不计。如果是这样,您可以使用 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

单选按钮组件

由于网页上没有单个单选按钮的用例,我们将创建一个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>
  )
}
Enter fullscreen mode Exit fullscreen mode

下拉组件

这个组件与之前的组件略有不同,因为我们将使用第三方插件。考虑到可访问性,我发现最好的插件是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>
  )
}
Enter fullscreen mode Exit fullscreen mode

按钮组件

构建组件的目的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>
  )
}
Enter fullscreen mode Exit fullscreen mode

验证表单

到目前为止,我们已经使表单具备了功能并能够提交表单数据。
接下来的需求是,我们需要为表单元素添加验证。我们将允许两种类型的验证:一种是简单地检查必填字段是否填写,另一种是通过提供一个模式来验证值。
例如,Input我们之前创建的组件现在将接收两个额外的 props:requiredvalidation

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

我们可以用同样的方式在其他组件中实现验证。

总结

在本文中,我们用最少的代码创建了组件。如果您想试用代码,请点击这里CodeSandbox 链接

您还可以在 GitHub 上找到可立即使用的此代码的TypeScript 版本(此处有演示)。

文章来源:https://dev.to/prasanjit/managing-react-forms-efficiently-at-scale-194i
PREV
什么是 GraphQL 以及为什么我应该使用它?
NEXT
常用Git命令汇总+Git使用难点场景解决方案