React 中管理表单的最佳设计模式
表单库 - 我需要一个吗?
最好的表单库是什么?
三层方法
进一步解释三层模式
这种模式的好处
这就是设计模式!
呃...为什么 React 中的表单代码总是这么混乱?
一开始很简单:一个<form>
组件,几个输入框,一个提交按钮。但很快事情就变得有点复杂了。
你想,“嗯……我想对这个邮政编码字段进行更多验证”。于是你添加了一个自定义的解决方法来验证输入字段中的数据。
然后,你想到“我想在表单提交时禁用提交按钮”。于是,你创建了另一个自定义方案,用于跟踪正在提交的内容、提交完成的时间等等。
然后,你又想:“我想要更好的错误处理”。于是你又添加了一个解决方法。
随着时间的推移,这个简单的表单会膨胀成一个长达 400 行的超级组件,具有多个 useEffect、useState 和自定义逻辑来处理所有边缘情况。
听起来很熟悉?
我遇到这个问题的次数比我愿意承认的要多得多。所以6个月前,我决定加倍努力,找到解决方案。我想知道:
在 React 中管理表单的最佳方法是什么,以使其井然有序、性能卓越且易于调试?
这就是我今天要在这里分享的内容。
表单库 - 我需要一个吗?
我以前也遇到过这种困境。由于项目规模较小,一开始的答案通常是“不行”,但随着时间的推移,答案不可避免地会转向“请,请,是的”。
所以现在,无论项目规模如何,我都提倡使用表单管理库。表单库通常包体相对较小,这对于代码组织至关重要。
但是,我应该注意:我过去也见过自定义表单管理工作。
问题在于这真的很难。虽然有可能,但即使成功了,你通常最终也会构建一个类似版本的表单库,只不过没有所有优秀的文档。
这就是为什么我建议你的项目从一开始就选择一个好的表单库。这就引出了下一个问题。
最好的表单库是什么?
这个决策过程本身就可以写成另一篇文章。但是,今天我想专注于具体的设计模式,所以我只会对现状做一个高层次的概述。
大量的表单管理库
React 中表单管理库的前景广阔。但幸运的是,它们集中在几个流行的库中。其中一些最受欢迎的库包括:react-hook-form、formik、redux form 和 react-final-form。
以下是它们受欢迎程度的细分,其中 Formik 最受欢迎,而 react-hook-form 紧随其后。
正如我之前提到的,我不会在本文中深入比较这些解决方案。但是,如果你想要一篇比较这些方案的优秀文章,请访问 https://retool.com/blog/choosing-a-react-form-library/。
话虽如此,我认为两个绝佳的表单库是Formik和React-Hook-Form。
两者都提供以钩子为中心的表单管理,并拥有出色的文档、活跃的开发人员和健康的用户群。
然而,在这两者之间,我倾向于 React-Hook-Form,我将在下面解释原因。
为什么选择 React-Hook-Form?
React-hook-form (RHF) 非常出色,因为它优先使用 hooks 来管理表单状态(因此得名)。如果您已经在使用 hooks,那么使用起来会更加快速、灵活、轻松。
在众多优势中,与 Formik 相比,react-hook-form 的一个优势在于它专为 hooks 而生。这意味着,尽管 react-hook-form 不支持类组件,但它们的文档和最佳实践更加专注。如果你在网上查找文章,你不会发现很多过时的指南和老旧的设计模式。我发现这一点在学习新库时非常有价值。
与其他库相比,它们在性能、打包和灵活性方面也拥有许多其他优势。以下仅列举一些示例:
这就是我选择 React-Hook-Form 的原因。但是,如果你的代码库使用了很多类组件,那么使用 Formik 可能更合适,因为它更容易集成到你的组件中。
我将在本文中介绍更高级的设计模式,但是如果您在任何时候感到困惑,这里有一些很好的资源可以帮助您理解 React-Hook-Form:官方入门指南、使用 RHF 和 Material UI以及RHF 与 5 种不同的 UI 库示例。
三层方法
三层方法的基本前提是将复杂的表单组件分成三个部分。
每个部分都将是独立的 React 组件,并专注于表单的一项职责(参见:SOLID)。每个部分都将以后缀(Apollo、Logic 或 View)命名,以便于查找。
以下是每个组件功能的概述:
阿波罗组件
此组件严格处理表单的网络请求(即获取表单的初始数据,并将最终数据提交到后端)。它被命名为“Apollo”,因为我通常使用 Apollo 与 GraphQL 后端通信。如果您愿意,可以使用更相关的后缀,例如:“API”、“Network”或“Fetch”。
逻辑组件
它处理表单的逻辑。您将在此组件中定义表单的形状、默认值和验证。
视图组件
此组件渲染表单的视图。它本应是一个无状态组件。不过,我通常会允许此组件包含与视图相关的状态,例如表单可扩展部分的 isOpen 切换按钮或类似的东西。
进一步解释三层模式
此图表展示了数据如何在这三层之间流动,从而创建井然有序的表单结构。从 Apollo.tsx 文件开始,沿着箭头了解数据如何在各个组件之间流动。
让我们更深入地了解一下这些组件。在这个例子中,我使用了 TypeScript,因为它有助于更好地理解传递的不同类型的数据。
另外,这里是完成的代码库。如果你是一个动手能力强的学习者,可以在阅读的同时随意尝试一下。
CreateUserApollo.tsx 详解
Apollo 组件负责通过网络获取表单数据。它的外观如下。
import { useQuery } from "react-query"; | |
import CreateUserLogic, { CreateUserFormModel } from "./CreateUserLogic"; | |
const CreateUserApollo = () => { | |
const { isLoading, data } = useQuery("createUserData", () => | |
fetch("https://jsonplaceholder.typicode.com/users/1").then((res) => | |
res.json() | |
) | |
); | |
const handleSubmit = async (data: CreateUserFormModel) => { | |
const submitData = { | |
name: data.username, | |
email: data.email, | |
createdAt: Date.now().toString() | |
}; | |
// return async function to submit data to backend | |
return fetch("https://httpbin.org/post", { | |
method: "POST", | |
headers: { | |
Accept: "application/json", | |
"Content-Type": "application/json" | |
}, | |
body: JSON.stringify(submitData) | |
}); | |
}; | |
// return early if initial form data isn't loaded | |
if (isLoading) return <div>Loading...</div>; | |
const defaultValues = { | |
username: data?.name ?? "", | |
email: data?.email ?? "" | |
}; | |
return ( | |
<CreateUserLogic defaultValues={defaultValues} onSubmit={handleSubmit} /> | |
); | |
}; | |
export default CreateUserApollo; |
关于这个组件我想指出几点。
首先,请注意从数据库获取的数据在传递到默认值之前是如何转换的。这一点很重要,因为通常情况下,最好不要信任通过网络获取的数据。如果不这样做,可能会出现以下三种错误。
(a)你最终可能会从 API 中获取过多的字段。这意味着你的表单将包含比实际需要更多的默认值。这会使你的表单更加混乱,并在验证时带来问题。
(b)这也能防止出现错误的默认值(例如 undefined)。与其完全信任后端,不如提供合理的默认值,例如空字符串,以防万一。
(c) 更健壮。注意到 API 中的用户字段在传递到表单之前是如何转换为用户名字段的吗?这对其他字段也很有用。例如,将后端的字符串时间戳解析为表单的 Date 对象。
我想指出的第二件事是关于 handleSubmit 函数的。该函数接收提交的表单数据,将其转换为 JSON 格式以供 API 使用,并返回一个异步函数,用于将结果更新到数据库中。
返回异步函数非常重要。稍后您会看到这一点,但本质上,它允许您在 CreateUserLogic 组件中等待 API 响应,这意味着您可以知道表单当前的提交状态。
CreateUserLogic.tsx 解释
该组件的目标很简单:使用默认值设置表单,将表单传递到视图层,然后在按下提交按钮时处理将表单提交给父组件。
这里我想重点指出的是 handleSubmit 函数。你应该记得 Apollo 组件也有一个 handleSubmit 函数。为什么需要两个呢?
这样做是为了保持我们三层的模块化。此组件中的 handleSubmit 函数允许你在表单成功提交后更改状态。它不关心数据如何提交,只关心提交何时完成。
相信我,我试过其他方法,最终你会发现这种方法最简洁。它让你无需关心其他层发生了什么,而是只需专注于它们所关心的事情。
在这个例子中,我们在提交后重置了表单。但是,你也可以轻松地使用它来路由到其他页面、显示成功提示、关闭模态框等等。这种设计模式让一切悬而未决,这很好。
另外,务必等待或返回 onSubmit(data) 函数。如果不这样做,一切仍然有效,但 react-hook-form 将无法感知提交过程何时完成,也无法正确处理表单的 isSubmitting 状态。
CreateUserView.tsx 详解
最后,我们得到了最简单的组件。它只是渲染你的表单字段。由于你已经完成了上层的所有复杂工作,所以这个组件可以非常简单。
这很棒,因为在大型表单中,这通常是最大的组件。此外,该组件仅处理表单的“外观”,不处理任何逻辑。这很棒,因为现在您可以轻松地将此文件交给设计师,而设计师无需关心表单的工作原理,他们只需关注它的外观。这太棒了!
这种模式的好处
好的,我在文章开头提到了我在创建表单时遇到的所有痛点。这个结构不仅解决了所有这些问题,还带来了一些其他好处。
✅ 为表单的每个步骤内置类型检查和验证
如果你注意到,逻辑组件包含每个字段的验证,并且该过程的每个步骤都具有强大的 TypeScript 类型。这使得它很难出错,并且更容易调试。
🔍 轻松找到事情发生的地方
您在向后端提交数据时遇到问题吗?很可能是 Apollo 组件的问题。字段默认值的问题?逻辑组件的问题。表单“外观”的问题?视图组件的问题。超级简单!
💨自动化测试轻而易举
这是该模式一个经常被忽视的优点。但是,如果你注意到的话,你可以通过将 props 直接传递给 Logic 组件来测试表单的功能。你完全不需要模拟后端,因为你可以完全绕过 Apollo 组件来测试所有功能。
🎁 表单变得更加可组合
这意味着您可以混合搭配不同的层,使表单的行为有所不同。您可以让不同的 Apollo 组件以不同的方式提交表单数据(例如,编辑文档与创建文档)。反之亦然,您可以将 Apollo 组件复用到不同的表单中,以便向相同的后端服务提交不同的数据。真是太酷了!
👥 易于团队分工
这种结构非常适合团队合作。你的设计师可以负责视图层,而后端人员可以负责 Apollo 组件。然后,你们可以轻松地在逻辑组件上进行协调,让你的新功能上线速度提升一倍!
这就是设计模式!
如您所见,将优秀的表单库与优秀的设计模式相结合,可以让杂乱的表单代码成为过去。它使协作更加轻松,开发更加简洁,调试更加快捷。还有什么理由不满意呢?
如果您有任何其他问题或改进,请发表评论!
文章来源:https://dev.to/spencerpauly/the-1-best-design-pattern-for-managing-forms-in-react-4215