Skip to content

高级用法

构建复杂且易于访问的表单

辅助功能 (A11y)

React Hook Form 支持原生表单验证,它允许你使用自己的规则验证输入。由于我们大多数人都必须使用自定义设计和布局来构建表单,因此我们有责任确保这些表单易于访问 (A11y)。

¥React Hook Form has support for native form validation, which lets you validate inputs with your own rules. Since most of us have to build forms with custom designs and layouts, it is our responsibility to make sure those are accessible (A11y).

以下代码示例按预期进行验证;然而,它可以在可访问性方面进行改进。

¥The following code example works as intended for validation; however, it can be improved for accessibility.

import React from "react"
import { useForm } from "react-hook-form"
export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
<input
id="name"
{...register("name", { required: true, maxLength: 30 })}
/>
{errors.name && errors.name.type === "required" && (
<span>This is required</span>
)}
{errors.name && errors.name.type === "maxLength" && (
<span>Max length exceeded</span>
)}
<input type="submit" />
</form>
)
}

以下代码示例是利用 ARIA 的改进版本。

¥The following code example is an improved version by leveraging ARIA.

import { useForm } from "react-hook-form"
export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
{/* use aria-invalid to indicate field contain error */}
<input
id="name"
aria-invalid={errors.name ? "true" : "false"}
{...register("name", { required: true, maxLength: 30 })}
/>
{/* use role="alert" to announce the error message */}
{errors.name && errors.name.type === "required" && (
<span role="alert">This is required</span>
)}
{errors.name && errors.name.type === "maxLength" && (
<span role="alert">Max length exceeded</span>
)}
<input type="submit" />
</form>
)
}

经过此改进后,屏幕阅读器会显示:“名称,编辑,无效条目,此为必填项。”

¥After this improvement, the screen reader will say: “Name, edit, invalid entry, This is required.”


向导表单/漏斗

通过不同的页面和部分收集用户信息是很常见的。我们建议使用状态管理库来存储通过不同页面或部分的用户输入。在这个例子中,我们将使用 小状态机 作为我们的状态管理库(如果你更熟悉的话可以用 redux 替换它)。

¥It's pretty common to collect user information through different pages and sections. We recommend using a state management library to store user input through different pages or sections. In this example, we are going to use little state machine as our state management library (you can replace it with redux if you are more familiar with it).

步骤 1:设置你的路由和存储。

¥Step 1: Set up your routes and store.

import { BrowserRouter as Router, Route } from "react-router-dom"
import { StateMachineProvider, createStore } from "little-state-machine"
import Step1 from "./Step1"
import Step2 from "./Step2"
import Result from "./Result"
createStore({
data: {
firstName: "",
lastName: "",
},
})
export default function App() {
return (
<StateMachineProvider>
<Router>
<Route exact path="/" component={Step1} />
<Route path="/step2" component={Step2} />
<Route path="/result" component={Result} />
</Router>
</StateMachineProvider>
)
}

第 2 步:创建页面,收集数据并将其提交到存储,然后推送到下一个表单/页面。

¥Step 2: Create your pages, collect and submit the data to the store and push to the next form/page.

import { useForm } from "react-hook-form"
import { withRouter } from "react-router-dom"
import { useStateMachine } from "little-state-machine"
import updateAction from "./updateAction"
const Step1 = (props) => {
const { register, handleSubmit } = useForm()
const { actions } = useStateMachine({ updateAction })
const onSubmit = (data) => {
actions.updateAction(data)
props.history.push("./step2")
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input type="submit" />
</form>
)
}
export default withRouter(Step1)

步骤 3:使用存储中的所有数据进行最终提交或显示结果数据。

¥Step 3: Make your final submission with all the data in the store or display the resulting data.

import { useStateMachine } from "little-state-machine"
import updateAction from "./updateAction"
const Result = (props) => {
const { state } = useStateMachine(updateAction)
return <pre>{JSON.stringify(state, null, 2)}</pre>
}

按照上述模式,你应该能够构建一个向导表单/漏斗来从多个页面收集用户输入数据。

¥Following the above pattern, you should be able to build a wizard form/funnel to collect user input data from multiple pages.


智能表单组件

这里的想法是,你可以轻松地使用输入来组成表单。我们将创建一个 Form 组件来自动收集表单数据。

¥This idea here is that you can easily compose your form with inputs. We are going to create a Form component to automatically collect form data.

import { Form, Input, Select } from "./Components"
export default function App() {
const onSubmit = (data) => console.log(data)
return (
<Form onSubmit={onSubmit}>
<Input name="firstName" />
<Input name="lastName" />
<Select name="gender" options={["female", "male", "other"]} />
<Input type="submit" value="Submit" />
</Form>
)
}

让我们看看每个组件都有什么。

¥Let's have a look what's in each of these components.

</> Form

Form 组件的职责是将所有 react-hook-form 方法注入到子组件中。

¥The Form component's responsibility is to inject all react-hook-form methods into the child component.

import React from "react"
import { useForm } from "react-hook-form"
export default function Form({ defaultValues, children, onSubmit }) {
const methods = useForm({ defaultValues })
const { handleSubmit } = methods
return (
<form onSubmit={handleSubmit(onSubmit)}>
{React.Children.map(children, (child) => {
return child.props.name
? React.createElement(child.type, {
...{
...child.props,
register: methods.register,
key: child.props.name,
},
})
: child
})}
</form>
)
}

</> Input / Select

这些输入组件的职责是将它们注册到 react-hook-form 中。

¥Those input components' responsibility is to register them into react-hook-form.

import React from "react"
export function Input({ register, name, ...rest }) {
return <input {...register(name)} {...rest} />
}
export function Select({ register, options, name, ...rest }) {
return (
<select {...register(name)} {...rest}>
{options.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
)
}

通过 Form 组件将 react-hook-formprops 注入到子组件中,你可以轻松地在应用中创建和编写复杂的表单。

¥With the Form component injecting react-hook-form's props into the child component, you can easily create and compose complex forms in your app.


错误信息

当用户的输入出现问题时,错误消息是向用户提供的视觉反馈。React Hook Form 提供了一个 errors 对象来让你轻松检索错误。有几种不同的方法可以改善屏幕上的错误显示。

¥Error messages are visual feedback to our users when there are issues with their inputs. React Hook Form provides an errors object to let you retrieve errors easily. There are several different ways to improve error presentation on the screen.

  • 注册

    ¥Register

    你可以简单地通过验证规则对象的 message 属性将错误消息传递给 register,如下所示:

    ¥You can simply pass the error message to register, via the message attribute of the validation rule object, like this:

    <input {...register('test', { maxLength: { value: 2, message: "error message" } })} />

  • 可选链接

    ¥Optional Chaining

    ?. 可选链接 运算符允许读取 errors 对象,而不必担心由于 nullundefined 导致另一个错误。

    ¥The ?. optional chaining operator permits reading the errors object without worrying about causing another error due to null or undefined.

    errors?.firstName?.message

  • Lodash get

    如果你的项目使用 lodash,那么你可以利用 lodash [get](https://lodash.com/docs/4.17.15#get) 功能。例如:

    ¥If your project is using lodash, then you can leverage the lodash [get](https://lodash.com/docs/4.17.15#get) function. Eg:

    get(errors, 'firstName.message')


连接表单

当我们构建表单时,有时我们的输入位于深度嵌套的组件树中,这就是 FormContext 派上用场的时候。然而,我们可以通过创建 ConnectForm 组件并利用 React 的 renderProps 来进一步改善开发者体验。好处是你可以更轻松地将输入与 React Hook Form 连接起来。

¥When we are building forms, there are times when our input lives inside of deeply nested component trees, and that's when FormContext comes in handy. However, we can further improve the Developer Experience by creating a ConnectForm component and leveraging React's renderProps. The benefit is you can connect your input with React Hook Form much easier.

import { FormProvider, useForm, useFormContext } from "react-hook-form"
export const ConnectForm = ({ children }) => {
const methods = useFormContext()
return children({ ...methods })
}
export const DeepNest = () => (
<ConnectForm>
{({ register }) => <input {...register("deepNestedInput")} />}
</ConnectForm>
)
export const App = () => {
const methods = useForm()
return (
<FormProvider {...methods}>
<form>
<DeepNest />
</form>
</FormProvider>
)
}

表单提供者性能

React Hook Form 的 FormProvider 是基于 React 的上下文 API 构建的。它解决了数据通过组件树传递的问题,而无需在每个级别手动向下传递 props。当 React Hook Form 触发状态更新时,这也会导致组件树触发重新渲染,但如果需要,我们仍然可以通过下面的示例优化我们的应用。

¥React Hook Form's FormProvider is built upon React's Context API. It solves the problem where data is passed through the component tree without having to pass props down manually at every level. This also causes the component tree to trigger a re-render when React Hook Form triggers a state update, but we can still optimise our App if required via the example below.

注意:在某些情况下,将 React Hook Form 的 开发工具FormProvider 一起使用可能会导致性能问题。在深入研究性能优化之前,首先考虑这个瓶颈。

¥Note: Using React Hook Form's Devtools alongside FormProvider can cause performance issues in some situations. Before diving deep in performance optimizations, consider this bottleneck first.

import React, { memo } from "react"
import { useForm, FormProvider, useFormContext } from "react-hook-form"
// we can use React.memo to prevent re-render except isDirty state changed
const NestedInput = memo(
({ register, formState: { isDirty } }) => (
<div>
<input {...register("test")} />
{isDirty && <p>This field is dirty</p>}
</div>
),
(prevProps, nextProps) =>
prevProps.formState.isDirty === nextProps.formState.isDirty
)
export const NestedInputContainer = ({ children }) => {
const methods = useFormContext()
return <NestedInput {...methods} />
}
export default function App() {
const methods = useForm()
const onSubmit = (data) => console.log(data)
console.log(methods.formState.isDirty) // make sure formState is read before render to enable the Proxy
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NestedInputContainer />
<input type="submit" />
</form>
</FormProvider>
)
}

受控与非受控组件混合

React Hook Form 包含非受控组件,但也与受控组件兼容。大多数 UI 库仅支持受控组件,例如 MUIAntd。但使用 React Hook Form,受控组件的重新渲染也得到了优化。这是一个将它们与验证结合起来的示例。

¥React Hook Form embraces uncontrolled components but is also compatible with controlled components. Most UI libraries are built to support only controlled components, such as MUI and Antd. But with React Hook Form, the re-rendering of controlled components are also optimized. Here is an example that combines them both with validation.

import React, { useEffect } from "react"
import { Input, Select, MenuItem } from "@material-ui/core"
import { useForm, Controller } from "react-hook-form"
const defaultValues = {
select: "",
input: "",
}
function App() {
const { handleSubmit, reset, watch, control, register } = useForm({
defaultValues,
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field }) => (
<Select {...field}>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
</Select>
)}
control={control}
name="select"
defaultValue={10}
/>
<Input {...register("input")} />
<button type="button" onClick={() => reset({ defaultValues })}>
Reset
</button>
<input type="submit" />
</form>
)
}
import React, { useEffect } from "react"
import { Input, Select, MenuItem } from "@material-ui/core"
import { useForm } from "react-hook-form"
const defaultValues = {
select: "",
input: "",
}
function App() {
const { register, handleSubmit, setValue, reset, watch } = useForm({
defaultValues,
})
const selectValue = watch("select")
const onSubmit = (data) => console.log(data)
useEffect(() => {
register({ name: "select" })
}, [register])
const handleChange = (e) => setValue("select", e.target.value)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Select value={selectValue} onChange={handleChange}>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
</Select>
<Input {...register("input")} />
<button type="button" onClick={() => reset({ ...defaultValues })}>
Reset
</button>
<input type="submit" />
</form>
)
}

使用解析器的自定义钩子

你可以构建自定义钩子作为解析器。自定义钩子可以轻松地与 yup/Joi/Superstruct 集成作为验证方法,并在验证解析器内部使用。

¥You can build a custom hook as a resolver. A custom hook can easily integrate with yup/Joi/Superstruct as a validation method, and to be used inside validation resolver.

  • 定义一个记忆的验证模式(或者如果你没有任何依赖,则在组件外部定义它)

    ¥Define a memorized validation schema (or define it outside your component if you don't have any dependencies)

  • 通过传递验证架构来使用自定义钩子

    ¥Use the custom hook, by passing the validation schema

  • 将验证解析器传递给 useForm 钩子

    ¥Pass the validation resolver to the useForm hook

import React, { useCallback, useMemo } from "react"
import { useForm } from "react-hook-form"
import * as yup from "yup"
const useYupValidationResolver = (validationSchema) =>
useCallback(
async (data) => {
try {
const values = await validationSchema.validate(data, {
abortEarly: false,
})
return {
values,
errors: {},
}
} catch (errors) {
return {
values: {},
errors: errors.inner.reduce(
(allErrors, currentError) => ({
...allErrors,
[currentError.path]: {
type: currentError.type ?? "validation",
message: currentError.message,
},
}),
{}
),
}
}
},
[validationSchema]
)
const validationSchema = yup.object({
firstName: yup.string().required("Required"),
lastName: yup.string().required("Required"),
})
export default function App() {
const resolver = useYupValidationResolver(validationSchema)
const { handleSubmit, register } = useForm({ resolver })
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input type="submit" />
</form>
)
}

使用虚拟化列表

想象一下你有一个数据表的场景。该表可能包含数百或数千行,每行都有输入。常见的做法是仅渲染视口中的项目,但这会导致问题,因为项目在视图之外并重新添加时会从 DOM 中删除。这将导致项目在重新进入视口时重置为其默认值。

¥Imagine a scenario where you have a table of data. This table might contain hundreds or thousands of rows, and each row will have inputs. A common practice is to only render the items that are in the viewport, however this will cause issues as the items are removed from the DOM when they are out of view and re-added. This will cause items to reset to their default values when they re-enter the viewport.

下面显示了使用 react-window 的示例。

¥An example is shown below using react-window.

import React from "react"
import { FormProvider, useForm, useFormContext } from "react-hook-form"
import { VariableSizeList as List } from "react-window"
import AutoSizer from "react-virtualized-auto-sizer"
import ReactDOM from "react-dom"
import "./styles.css"
const items = Array.from(Array(1000).keys()).map((i) => ({
title: `List ${i}`,
quantity: Math.floor(Math.random() * 10),
}))
const WindowedRow = React.memo(({ index, style, data }) => {
const { register } = useFormContext()
return <input {...register(`${index}.quantity`)} />
})
export const App = () => {
const onSubmit = (data) => console.log(data)
const formMethods = useForm({ defaultValues: items })
return (
<form className="form" onSubmit={formMethods.handleSubmit(onSubmit)}>
<FormProvider {...formMethods}>
<AutoSizer>
{({ height, width }) => (
<List
height={height}
itemCount={items.length}
itemSize={() => 100}
width={width}
itemData={items}
>
{WindowedRow}
</List>
)}
</AutoSizer>
</FormProvider>
<button type="submit">Submit</button>
</form>
)
}
import { FixedSizeList } from "react-window"
import { Controller, useFieldArray, useForm } from "react-hook-form"
const items = Array.from(Array(1000).keys()).map((i) => ({
title: `List ${i}`,
quantity: Math.floor(Math.random() * 10),
}))
function App() {
const { control, getValues } = useForm({
defaultValues: {
test: items,
},
})
const { fields, remove } = useFieldArray({ control, name: "test" })
return (
<FixedSizeList
width={400}
height={500}
itemSize={40}
itemCount={fields.length}
itemData={fields}
itemKey={(i) => fields[i].id}
>
{({ style, index, data }) => {
const defaultValue =
getValues()["test"][index].quantity ?? data[index].quantity
return (
<form style={style}>
<Controller
render={({ field }) => <input {...field} />}
name={`test[${index}].quantity`}
defaultValue={defaultValue}
control={control}
/>
</form>
)
}}
</FixedSizeList>
)
}

测试表单

测试非常重要,因为它可以防止代码出现错误或错误。它还保证了重构代码库时的代码安全。

¥Testing is very important because it prevents your code from having bugs or mistakes. It also guarantees code safety when refactoring the codebase.

我们推荐使用 testing-library,因为它简单并且测试更关注用户行为。

¥We recommend using testing-library, because it is simple and tests are more focused on user behavior.

步骤 1:设置你的测试环境。

¥Step 1: Set up your testing environment.

请安装 @testing-library/jest-dom 和最新版本的 jest,因为 react-hook-form 使用 MutationObserver 来检测输入,并从 DOM 中卸载。

¥Please install @testing-library/jest-dom with the latest version of jest, because react-hook-form uses MutationObserver to detect inputs, and to get unmounted from the DOM.

注意:如果你使用 React Native,则不需要安装 @testing-library/jest-dom

¥Note: If you are using React Native, you don't need to install @testing-library/jest-dom.

npm install -D @testing-library/jest-dom

创建 setup.js 以导入 @testing-library/jest-dom

¥Create setup.js to import @testing-library/jest-dom.

import "@testing-library/jest-dom"

注意:如果你使用的是 React Native,则需要创建 setup.js,定义 window 对象,并在安装文件中包含以下行:

¥Note: If you are using React Native, you need to create setup.js, define window object, and include the following lines in the setup file:

global.window = {}
global.window = global

最后,你必须更新 jest.config.js 中的 setup.js 以包含该文件。

¥Finally, you have to update setup.js in jest.config.js to include the file.

module.exports = {
setupFilesAfterEnv: ["<rootDir>/setup.js"], // or .ts for TypeScript App
// ...other settings
}

此外,你可以设置 eslint-plugin-testing-libraryeslint-plugin-jest-dom 以遵循最佳实践并预测编写测试时的常见错误。

¥Additionally, you can set up eslint-plugin-testing-library and eslint-plugin-jest-dom to follow best practices and anticipate common mistakes when writing your tests.

第 2 步:创建登录表单。

¥Step 2: Create login form.

我们已经相应地设置了角色属性。这些属性对你编写测试很有帮助,并且可以提高可访问性。更多信息,你可以参考 testing-library 文档。

¥We have set the role attribute accordingly. These attributes are helpful for when you write tests, and they improve accessibility. For more information, you can refer to the testing-library documentation.

import React from "react"
import { useForm } from "react-hook-form"
export default function App({ login }) {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm()
const onSubmit = async (data) => {
await login(data.email, data.password)
reset()
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">email</label>
<input
id="email"
{...register("email", {
required: "required",
pattern: {
value: /\S+@\S+\.\S+/,
message: "Entered value does not match email format",
},
})}
type="email"
/>
{errors.email && <span role="alert">{errors.email.message}</span>}
<label htmlFor="password">password</label>
<input
id="password"
{...register("password", {
required: "required",
minLength: {
value: 5,
message: "min length is 5",
},
})}
type="password"
/>
{errors.password && <span role="alert">{errors.password.message}</span>}
<button type="submit">SUBMIT</button>
</form>
)
}

步骤 3:编写测试。

¥Step 3: Write tests.

我们试图通过测试涵盖以下标准:

¥The following criteria are what we try to cover with the tests:

  • 测试提交失败。

    ¥Test submission failure.

    我们使用 waitFor util 和 find* 查询来检测提交反馈,因为 handleSubmit 方法是异步执行的。

    ¥We are using waitFor util and find* queries to detect submission feedback, because the handleSubmit method is executed asynchronously.

  • 测试与每个输入相关的验证。

    ¥Test validation associated with each inputs.

    我们在查询不同元素时使用 *ByRole 方法,因为这是用户识别你的 UI 组件的方式。

    ¥We are using the *ByRole method when querying different elements because that's how users recognize your UI component.

  • 测试提交成功。

    ¥Test successful submission.

import React from "react"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import App from "./App"
const mockLogin = jest.fn((email, password) => {
return Promise.resolve({ email, password })
})
it("should display required error when value is invalid", async () => {
render(<App login={mockLogin} />)
fireEvent.submit(screen.getByRole("button"))
expect(await screen.findAllByRole("alert")).toHaveLength(2)
expect(mockLogin).not.toBeCalled()
})
it("should display matching error when email is invalid", async () => {
render(<App login={mockLogin} />)
fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
target: {
value: "test",
},
})
fireEvent.input(screen.getByLabelText("password"), {
target: {
value: "password",
},
})
fireEvent.submit(screen.getByRole("button"))
expect(await screen.findAllByRole("alert")).toHaveLength(1)
expect(mockLogin).not.toBeCalled()
expect(screen.getByRole("textbox", { name: /email/i })).toHaveValue("test")
expect(screen.getByLabelText("password")).toHaveValue("password")
})
it("should display min length error when password is invalid", async () => {
render(<App login={mockLogin} />)
fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
target: {
value: "test@mail.com",
},
})
fireEvent.input(screen.getByLabelText("password"), {
target: {
value: "pass",
},
})
fireEvent.submit(screen.getByRole("button"))
expect(await screen.findAllByRole("alert")).toHaveLength(1)
expect(mockLogin).not.toBeCalled()
expect(screen.getByRole("textbox", { name: /email/i })).toHaveValue(
"test@mail.com"
)
expect(screen.getByLabelText("password")).toHaveValue("pass")
})
it("should not display error when value is valid", async () => {
render(<App login={mockLogin} />)
fireEvent.input(screen.getByRole("textbox", { name: /email/i }), {
target: {
value: "test@mail.com",
},
})
fireEvent.input(screen.getByLabelText("password"), {
target: {
value: "password",
},
})
fireEvent.submit(screen.getByRole("button"))
await waitFor(() => expect(screen.queryAllByRole("alert")).toHaveLength(0))
expect(mockLogin).toBeCalledWith("test@mail.com", "password")
expect(screen.getByRole("textbox", { name: /email/i })).toHaveValue("")
expect(screen.getByLabelText("password")).toHaveValue("")
})

解决测试期间的行为警告

¥Resolving act warning during test

如果你测试使用 react-hook-form 的组件,即使你没有为该组件编写任何异步代码,你也可能会遇到这样的警告:

¥If you test a component that uses react-hook-form, you might run into a warning like this, even if you didn't write any asynchronous code for that component:

警告:测试中对 MyComponent 的更新未包含在 act(...) 中

¥Warning: An update to MyComponent inside a test was not wrapped in act(...)

import React from "react"
import { useForm } from "react-hook-form"
export default function App() {
const { register, handleSubmit, formState } = useForm({
mode: "onChange",
})
const onSubmit = (data) => {}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("answer", {
required: true,
})}
/>
<button type="submit">SUBMIT</button>
</form>
)
}
import React from "react"
import { render, screen } from "@testing-library/react"
import App from "./App"
it("should have a submit button", () => {
render(<App />)
expect(screen.getByText("SUBMIT")).toBeInTheDocument()
})

在此示例中,有一个简单的表单,没有任何明显的异步代码,并且测试仅渲染组件并测试按钮是否存在。但是,它仍然记录有关更新未包含在 act() 中的警告。

¥In this example, there is a simple form without any apparent async code, and the test merely renders the component and tests for the presence of a button. However, it still logs the warning about updates not being wrapped in act().

这是因为 react-hook-form 内部使用异步验证处理程序。为了计算 formState,它必须首先验证表单,这是异步完成的,从而导致另一个渲染。该更新发生在测试函数返回后,从而触发警告。

¥This is because react-hook-form internally uses asynchronous validation handlers. In order to compute the formState, it has to initially validate the form, which is done asynchronously, resulting in another render. That update happens after the test function returns, which triggers the warning.

要解决此问题,请等待 UI 中的某些元素与 find* 查询一起出现。请注意,你不得将 render() 调用封装在 act() 中。你可以在此处阅读更多有关在 act 中不必要地封装内容的信息

¥To solve this, wait until some element from your UI appears with find* queries. Note that you must not wrap your render() calls in act(). You can read more about wrapping things in act unnecessarily here.

import React from "react"
import { render, screen } from "@testing-library/react"
import App from "./App"
it("should have a submit button", async () => {
render(<App />)
expect(await screen.findByText("SUBMIT")).toBeInTheDocument()
// Now that the UI was awaited until the async behavior was completed,
// you can keep asserting with `get*` queries.
expect(screen.getByRole("textbox")).toBeInTheDocument()
})

转换和解析

原生输入返回 string 格式的值,除非使用 valueAsNumbervalueAsDate 调用,你可以在 本节 下阅读更多内容。然而,它并不完美。我们仍然需要处理 isNaNnull 值。因此最好将转换保留在自定义钩子级别。在以下示例中,我们使用 Controller 来包含变换值的输入和输出的功能。你还可以使用自定义 register 获得类似的结果。

¥The native input returns the value in string format unless invoked with valueAsNumber or valueAsDate, you can read more under this section. However, it's not perfect. We still have to deal with isNaN or null values. So it's better to leave the transform at the custom hook level. In the following example, we are using the Controller to include the functionality of the transform value's input and output. You can also achieve a similar result with a custom register.

const ControllerPlus = ({
control,
transform,
name,
defaultValue
}) => (
<Controller
defaultValue={defaultValue}
control={control}
name={name}
render={({ field }) => (
<input
onChange={(e) => field.onChange(transform.output(e))}
value={transform.input(field.value)}
/>
)}
/>
);
// usage below:
<ControllerPlus<string, number>
transform={{
input: (value) =>
isNaN(value) || value === 0 ? "" : value.toString(),
output: (e) => {
const output = parseInt(e.target.value, 10);
return isNaN(output) ? 0 : output;
}
}}
control={control}
name="number"
defaultValue=""
/>
React Hook Form 中文网 - 粤ICP备13048890号