13|Form:Hooks给Form处理带来了哪些新变化?
文章目录
你好,我是王沛。今天我们来聊聊如何在 React 中使用表单。
表单作为用户交互最为常见的形式,但在 React 中实现起来却并没有那么容易。甚至可以说,使用表单,是 React 开发中最为困难的一部分。
主要有两方面的原因。一方面,React 都是状态驱动,而表单却是事件驱动,比如点击按钮、输入字符等,都是一个个离散的事件。因此,一般来说我们都需要将这些独立的事件转换成一定的应用程序状态,最终来完成表单的交互。
另一方面,表单元素一般都有自己的内在状态,比如原生的 input 节点就允许用户输入,这就需要我们在元素状态和表单状态之间做同步。
要能够很好地处理这些问题,我们首先需要对表单的机制有深入的理解,然后再从 React Hooks 的角度去思考问题的解决方案。
所以在今天这节课,我会从这三个方面来讲。
- 首先,介绍 React 中使用表单的基本原理,帮助你理解受控组件和非受控组件的概念,以及各自的适用场景。
- 然后看看 Hooks 出现后,给表单处理带来了哪些思路上的新变化。
- 最后,我们会学习几个常见的开箱即用的 React 表单解决方案,让你在理解实现原理的基础上,可以选择最适合自己的开源方案。
在表单中使用 React 组件:受控组件和非受控组件
在第 8 讲,我简单介绍了受控组件和非受控组件的概念。虽然在一般情况下,表单中的元素都是受控组件。也就是说,一个表单组件的状态完全由 React 管控。但是在有的时候,为了避免太多的重复渲染,我们也会选择非受控组件。
所以今天这节课,我们就来看下这两种形式在 React 中分别该怎么实现,并了解它们的优缺点以及适用场景。
首先我们来看下受控组件应该如何使用。下面的例子展示了受控组件的用法:
function MyForm() {
const [value, setValue] = useState(’’);
const handleChange = useCallback(evt => {
setValue(evt.target.value);
}, []);
return ;
}
可以看到,输入框的值是由传入的 value 属性决定的。在 onChange 的事件处理函数中,我们设置了 value 这个状态的值,这样输入框就显示了用户的输入。
需要注意的是,React 统一了表单组件的 onChange 事件,这样的话,用户不管输入什么字符,都会触发 onChange 事件。而标准的 input 的 onchange 事件,则只有当输入框失去焦点时才会触发。React 的这种 onChange 的机制,其实让我们对表单组件有了更灵活的控制。
不过,受控组件的这种方式虽然统一了表单元素的处理,有时候却会产生性能问题。因为用户每输入一个字符,React 的状态都会发生变化,那么整个组件就会重新渲染。所以如果表单比较复杂,那么每次都重新渲染,就可能会引起输入的卡顿。在这个时候,我们就可以将一些表单元素使用非受控组件去实现,从而避免性能问题。
所谓非受控组件,就是表单元素的值不是由父组件决定的,而是完全内部的状态。联系第 8 讲提到的唯一数据源的原则,一般我们就不会再用额外的 state 去保存某个组件的值。而是在需要使用的时候,直接从这个组件获取值。
下面这段代码演示了一个非受控组件应该如何使用:
import { useRef } from “react”;
export default function MyForm() {
// 定义一个 ref 用于保存 input 节点的引用
const inputRef = useRef();
const handleSubmit = (evt) => {
evt.preventDefault();
// 使用的时候直接从 input 节点获取值
alert(“Name: " + inputRef.current.value);
};
return (
);
}
可以看到,通过非受控组件的方式,input 的输入过程对整个组件状态没有任何影响,自然也就不会导致组件的重新渲染。
不过缺点也是明显的,输入过程因为没有对应的状态变化,因此要动态地根据用户输入做 UI 上的调整就无法做到了。出现这种情况,主要也是因为所有的用户输入都是 input 这个组件的内部状态,没有任何对外的交互。
总结来说,在实际的项目中,我们一般都是用的受控组件,这也是 React 官方推荐的使用方式。不过对于一些个别的场景,比如对性能有极致的要求,那么非受控组件也是一种不错的选择。
使用 Hooks 简化表单处理
回顾我们对受控组件的处理,会发现对于每一个表单元素,其实都会遵循下面两个步骤:
- 设置一个 State 用于绑定到表单元素的 value;
- 监听表单元素的 onChange 事件,将表单值同步到 value 这个 state。
也就是说,我们可以用类似如下的代码来实现对受控组件的处理:
function MyForm() {
const [value1, setValue1] = useState();
const [value2, setValue2] = useState();
// 更多表单元素状态…
return (
)
}
可以看到,一个表单的整体状态正是有一个个独立表单元素共同组成的。那么下图就展示了这样一个关系,对于每一个表单元素都需要手动的进行 value 和 onChange 属性的绑定:
对于复杂的表单,这样的逻辑显然是比较繁琐的。但正如上图所示,维护表单组件的状态逻辑,核心在于三个部分:
- 字段的名字;
- 绑定 value 值;
- 处理 onChange 事件。
既然对每个表单元素的处理逻辑都是一致的,那我们是不是可以用 Hooks 实现逻辑的重用呢?
答案是肯定的,主要的思想就是在这个 Hook 去维护整个表单的状态,并提供根据名字去取值和设值的方法,从而方便表单在组件中的使用。下面的代码演示了一个基本的实现:
import { useState, useCallback } from “react”;
const useForm = (initialValues = {}) => {
// 设置整个 form 的状态:values
const [values, setValues] = useState(initialValues);
// 提供一个方法用于设置 form 上的某个字段的值
const setFieldValue = useCallback((name, value) => {
setValues((values) => ({
…values,
[name]: value,
}));
}, []);
// 返回整个 form 的值以及设置值的方法
return { values, setFieldValue };
};
有了这样一个简单的 Form,我们就不再需要很繁琐地为每个表单元素单独设置状态了。比如下面的代码就演示了该如何使用这样一个 Hook:
import { useCallback } from “react”;
import useForm from ‘./useForm’;
export default () => {
// 使用 useForm 得到表单的状态管理逻辑
const { values, setFieldValue } = useForm();
// 处理表单的提交事件
const handleSubmit = useCallback(
(evt) => {
// 使用 preventDefault() 防止页面被刷新
evt.preventDefault();
console.log(values);
},
[values],
);
return (
可以看到,Formik 将所有的表单状态都,通过 render props 的回调函数传递给了表单的 UI 展现层。这样,你就可以根据这些状态进行 UI 的渲染。当然,在 Hooks 出现之后,Formik 也提供了 Hook 的 API 去实现表单逻辑。
另外一点就是,Formik 只提供了表单状态逻辑的重用,并没有限制使用何种 UI 库,这就意味着 Formik 在提供灵活性的同时,也意味着你要自己管理如何进行 UI 布局以及错误信息的展示。Formik 的项目地址是:https://formik.org/。
React Hook Form
React Hook Form,顾名思义,就是在 Hooks 出现之后,完全基于 Hooks 实现的表单状态管理框架。
区别于 antd 和 formik 的一个最大特点是,React Hook Form 是通过非受控组件的方式进行表单元素的管理。正如上文提到,这可以避免很多的表单重新渲染,从而对于复杂的表单组件可以避免性能问题。
此外,和 formik 一样,React Hook Form 也没有绑定到任何 UI 库,所以你同样需要自己处理布局和错误信息的展示。它的项目地址是:https://react-hook-form.com/。
小结
在这节课,我们学习了在 React 中使用 Form 的基本流程。Form 最为核心的机制就是我们将表单元素的所有状态提取出来,这样表单就可以分为状态逻辑和 UI 展现逻辑,从而实现数据层和表现层的分离。
在一些传统的表单解决方案之中,这个状态一般会用 Context 或者 Redux 去管理,而现在有了 Hooks,那我们基于 Hooks 就可以很容易实现一个简单的表单逻辑管理模块。
而对于一些流行的表单解决方案,其实也就是在 Hooks 的基础上,加入了更多的便捷 API,比如更丰富的验证逻辑,来简化 React 中表单的创建。在理解了背后的原理之后,相信你能利用已有的方案提高开发的效率。
文章中所有的实例代码你也可以通过下面地址进行查看:https://codesandbox.io/s/react-hooks-course-20vz 。
思考题
今天有两个思考题:
- 原生表单有一个重要的机制:重置(reset)。在文中的自定义 useForm Hook 中,如果要提供 reset 的 API,你会如何实现呢?
- 同样的,在文中自定义 useForm 的 Hook 中,表单的验证逻辑是通过一个同步调用的函数实现的,如果要用支持异步验证,比如通过服务器端 API 判断 name 是否已存在,应该如何实现呢?
欢迎把你的思考和想法分享在留言区,我会和你交流。同时,我也会把其中一些不错的回答置顶,供大家学习。
文章作者 anonymous
上次更新 2024-05-19