问题
大多数React应用程序都或多或少地使用着表单,而且大多数应用程序都采用不同的方式来创建表单。如果和你一起开发应用的是一整个团队,你可能会发现一些开发人员在不同的状态变量中管理各个字段。另一些人则会选择在单值对象中记录表单状态,这更容易将数值传入和传出表单,但要更新每个字段可能会比较麻烦。表单验证通常会导致代码变得复杂,有些表单在提交时验证,有些表单在用户输入时动态验证。有些表单可能在首次加载时显示验证消息。还有一些表单,验证消息可能只在用户与表单字段交互时出现。
这些设计上的差异会导致用户体验的降低和代码风格的不一致。根据我们与React团队合作的经验,表单和表单验证是开发者经常遇到的挑战。
解决方案
为了在表单开发中保持一致性,我们将创建一个SimpleForm组件,用来将一个或多个InputField组件包裹起来。下面是一个使用SimpleForm和InputField的例子:
我们在对象formFields中跟踪表单的状态。当我们更改表单中的字段时,该字段将调用SimpleForm上的onChange。field1字段使用onValidate方法进行验证,当验证状态改变时,该字段调用SimpleForm上的onValid方法。当用户与某个字段交互时,会标记为 dirty ,此时才会进行验证。
具体的表单如图3-11所示。
你不需要手动跟踪各个字段值。表单值对象用字段名称作为属性来记录单个字段值。InputField处理何时进行验证的细节:它将更新表单值并决定何时显示错误信息。
图3-11:带有字段验证的简单表单
图3-12显示了一个稍微复杂一点的示例,它使用了带有几个字段的SimpleForm。
图3-12:更复杂的表单
要创建SimpleForm和InputField组件,我们首先要了解它们相互之间如何通信。InputField组件需要告诉SimpleForm它的值什么时候发生了变化,以及新值是否有效。它将通过context来实现这一点。
context是一个存储上下文的地方。当组件在context中存储值时,该值对其子组件是可见的。SimpleForm将创建一个名为FormContext的context,并使用它来存储一组回调函数,任何子组件都可以使用这些回调函数与表单通信:
为了了解SimpleForm是如何工作的,让我们从一个简化版本开始,只关心子组件的值,不用考虑表单验证:
SimpleForm的最终版本将会加入用于跟踪验证和错误信息的代码,但是简化后的表单将更容易理解。
表单将跟踪values对象中的所有字段值。表单创建了名为getValue和setValue的两个回调函数,并将它们放入context中(作为form对象),子组件将在context中获取这两个回调函数。我们通过用<FormContext.Provider>包裹子组件来把form放在context中。
请注意,我们在useCallback中包裹了getValue和setValue回调,这将防止组件在每次渲染SimpleForm时重新创建这两个函数。
每当子组件调用form.value()函数时,它将收到指定字段的当前值。如果子组件调用form.setValue(),那么它将更新指定字段的值。
现在让我们看一下简化版的InputField组件,为便于理解,我们同样删除了表单验证相关代码:
InputField通过FormContext获取form对象。如果form对象是空值,我们就知道InputField组件并没有被包裹在SimpleForm组件中。然后,InputField将渲染一个input类型的字段,将其值设置为form.value(name)返回的值。如果用户更改了字段的值,则InputField组件将新值发送给form.setValue(name,event.target.value)。
如果你需要一个除了input之外的其他表单字段,你可以将其包裹在与这里的InputField类似的组件中。
表单验证代码大同小异。与表单在values状态中跟踪其当前值的方式相同,它还需要跟踪哪些字段是被标记为dirty的,哪些字段是未通过验证的。然后它需要传递setDirty、isDirty和setInvalid的回调函数,这些回调由子字段在调用onValidate时使用。
下面是SimpleForm组件的最终版本,包括验证代码:
下面是InputField组件的最终版本。注意,字段一旦失去焦点或值发生变化,就会被标记为 dirty :
讨论
你可以使用此解决方案创建许多简单的表单,还可以举一反三将其用于任何React组件。例如,如果你正在使用第三方的日历或日期选择器,则只需要将其包装在类似于InputField的组件中,就可以在SimpleForm中使用它。
此解决方案不支持表单中的表单或表单数组。你应该可以通过修改SimpleForm组件来使其行为类似于InputField,以便将一个表单放到另一个表单中。
你可以从GitHub网站( https://oreil.ly/gU03F )下载本解决方案的源代码。