React 使用Hooks简化受控组件的状态绑定


Posted in Javascript onMarch 18, 2019

开始之前

阅读本文需要对以下几项有一定了解

ECMAScript 6

文章中大量用到了 ES6 语法,比如解构赋值和函数参数默认值、剩余参数、展开语法、箭头函数等。

Hooks

React 在 16.8 版本中推出了 Hooks,它允许你在“函数组件”中使用“类组件”的一些特性。

React 本身提供了一些 Hooks,比如 useState、useReducer 等。通过在一个以“use”作为命名起始的函数中调用这些 Hooks,就得到了一个 custom Hook(自定义 Hook)。

Custom Hooks 允许我们把任何逻辑封装到其中,以便于复用足够小的组件逻辑。

Controlled Components

当我们把像 <input> <textarea> 和 <select> 这样的 HTML 元素本身的状态交给 React state 去管理,我们就得到了一个“受控组件”。

styled-components

一个与 React 契合良好的 CSS in JS 库。它允许你使用 JS 编写样式,并编译成纯 CSS 文件。
下面代码中所有的样式都是使用它编写的。如果对代码中样式的实现不是很感兴趣的话, 这个可以跳过。

代码实现

Input 组件

首先我们需要实现一个 Input 组件,我们将在该组件的基础上进行输入、校验并提示。

Input.js

import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

const Wrap = styled.div({
 display: 'flex',
 flexDirection: 'column',

 label: { display: 'flex', alignItems: 'center' },

 input: {
  marginLeft: 8,
 },

 p: {
  color: 'red',
 },
});

function Input({ label, type, helperText, error, ...otherProps }) {
 return (
  <Wrap>
   <label>
    {label}:
    <input {...otherProps} type={type} />
   </label>
   {error && <p>{helperText}</p>}
  </Wrap>
 );
}

Input.propTypes = {
 label: PropTypes.string,
 type: PropTypes.string,
 helperText: PropTypes.string,
 error: PropTypes.bool,
};

export default Input;

该组件主要接收以下几个 props:

  • label label 标签的文本
  • type 赋值给原生 input 标签的 type 属性
  • error 数据类型为 Boolean,如果为 true 则表示当前表单域有错误,即验证不通过
  • helperText 当前表单域验证不通过时,显示在表单域下方的提示文字
  • otherProps props 中除了上述四个以外的其他属性,全部赋值给原生 input 标签

Custom Hook

有了 UI 组件之后,就可以开始实现我们的自定义 Hook 了。

useInput.js

import { useState } from 'react';

export default function useInput({
 initValue = '',
 helperText = '',
 validator = () => true,
 validateTriggers = ['onChange'],
} = {}) {
 // 保存用户输入的值,使用 initValue 作为初始值
 const [value, setValue] = useState(initValue);
 // Boolean 类型,表示当前表单项的验证状态
 const [error, setError] = useState(false);

 function onChange(e) {
  const { value } = e.target;

  setValue(value);

  // 根据 validateTriggers 的选项,决定是否要在 onChange 里进行校验
  if (validateTriggers.includes('onChange')) {
   setError(!validator(value));
  }
 }

 /**
  * 根据 validateTriggers 生成相应的事件处理器
  */
 function createEventHandlers() {
  const eventHandlers = {};

  validateTriggers.forEach(item => {
   // 生成相应的事件处理器,并在其中做输入校验。
   eventHandlers[item] = e => {
    const { value } = e.target;
    setError(!validator(value));
   };
  });

  return eventHandlers;
 }

 const eventHandlers = createEventHandlers();

 return {
  value,
  helperText,
  error,
  ...eventHandlers,
  onChange,
 };
}

useInput 接收一个 options 对象作为参数,考虑到扩展性,使用一个配置对象作为参数比较好。

options 对象拥有以下几个属性:

  • initValue 输入框的初始值
  • helperText 当表单验证不通过时显示的字符串
  • validator 用于进行表单验证的函数,接收 value 作为参数,并返回一个 Boolean 值,表示表单验证是否通过
  • validateTriggers 字符串数组,表明在哪个或哪几个事件中调用 validator 进行输入校验。

在函数体中,我们调用两次 useState 来初始化 value 和 error 的值,分别保存用户输入的值和当前表单域的校验结果。
然后,声明一个 onChange 方法用来绑定 input 元素的 change 事件,在该方法中,我们把用户输入的值赋值给 value,同时根据 validateTriggers 的值,决定是否要在该方法中进行输入校验。该方法随后会被返回出去,再作为 props 传递给相应的组件,完成受控组件的状态绑定。

我们还需要声明一个 createEventHandlers 方法,该方法通过遍历 validateTriggers,生成相应的事件处理器,并在这些事件处理器中进行输入校验。

最后我们调用 createEventHandlers 方法,并把生成的 eventHandlers(事件处理器) 通过扩展运算符,插入到最终返回的对象中。

注意:这里我们需要把 onChange 放在最后,以免带有状态绑定的 onChange 方法被 eventHandlers 中的 onChange 覆盖掉。

具体使用

现在让我们来看看实际该如何使用:

import React from 'react';
import Input from './Input';
import useInput from './useInput';

// 用于验证邮箱的正则表达式
const EMAIL_REG = /\S+@\S+\.\S+/;

export default function Form() {
 const email = useInput({
  initValue: '',
  helperText: '请输入有效的邮箱!',
  validator: value => EMAIL_REG.test(value),
  validateTriggers: ['onBlur'],
 });

 const password = useInput({
  initValue: '',
  helperText: '密码长度需要在 6-20 之间!',
  validator: value => value.length >= 6 && value.length <= 20,
  validateTriggers: ['onChange', 'onBlur'],
 });

 /**
  * 判断是否禁用按钮
  */
 function isButtonDisabled() {
  // 当邮箱或密码未填写时,或者邮箱或密码输入校验未通过时,禁用按钮
  return !email.value || !password.value || email.error || password.error;
 }

 /**
  * 处理表单提交
  */
 function handleButtonClick() {
  console.log('邮箱:', email.value);
  console.log('密码:', password.value);
 }

 return (
  <div>
   <Input {...email} label="邮箱" type="email" />
   <Input {...password} label="密码" type="password" />

   <button disabled={isButtonDisabled()} onClick={handleButtonClick}>
    登录
   </button>
  </div>
 );
}

这里调用了两次 useInput,初始化 email 和 password 表单域数据。

然后使用扩展运算符,把值全部赋给 Input 组件。只用了几行代码就完成了定义初始值和受控组件的绑定,是不是很方便?

当我们输入邮箱的时候,并不会出现校验提示,但是一旦从邮箱输入框失去焦点以后,输入的值就会被校验,并根据校验结果显示相应的提示。而密码输入框,则会在输入的过程中和失焦后都进行校验。

总结

上面这个例子已经可以处理基本的表单验证,至于格式化用户输入的数据以及自定义收集表单域的值的时机等其他需求,我就不再演示了,大家可以自行设计。这也是 Hooks 的特殊之处,它让我们可以更容易的复用逻辑代码,可以根据需要自行编写 custom Hooks。

文章中关于 useInput 的 API 设计只是众多方案中的一种,只是为大家提供一些参考。你也可以把整个表单的状态封装到一个 useForm 方法中,统一管理所有表单域的状态。

希望本文能为大家带来一些关于如何使用 Hooks 的灵感,即使从来没有使用过 Hooks,也强烈建议大家尝试一下。我已经在项目中大量使用 Hooks 了,并且它也为我带来了很好的效果。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript中的常见排序算法
Mar 27 Javascript
用JTrackBar实现的模拟苹果风格的滚动条
Aug 06 Javascript
基于Jquery的标签智能验证实现代码
Dec 27 Javascript
Json字符串转换为JS对象的高效方法实例
May 01 Javascript
用jquery方法操作radio使其默认选项是否
Sep 10 Javascript
jquery next nextAll nextUntil siblings的区别介绍
Oct 05 Javascript
JavaScript实现文字与图片拖拽效果的方法
Feb 16 Javascript
jQuery实现简单的日期输入格式化控件
Mar 12 Javascript
jquery实现简单Tab切换菜单效果
Jul 17 Javascript
js使用原型对象(prototype)需要注意的地方
Aug 28 Javascript
Django与Vue语法的冲突问题完美解决方法
Dec 14 Javascript
基于vue和websocket的多人在线聊天室
Feb 01 Javascript
JavaScript显式数据类型转换详解
Mar 18 #Javascript
浅谈js中的bind
Mar 18 #Javascript
node微信开发之获取access_token+自定义菜单
Mar 17 #Javascript
JavaScript中this用法学习笔记
Mar 17 #Javascript
通过JavaScript下载文件到本地的方法(单文件)
Mar 17 #Javascript
微信小程序登录session的使用
Mar 17 #Javascript
Javascript读写cookie的实例源码
Mar 16 #Javascript
You might like
PHP个人网站架设连环讲(一)
2006/10/09 PHP
2个Codeigniter文件批量上传控制器写法例子
2014/07/25 PHP
php在linux下检测mysql同步状态的方法
2015/01/15 PHP
YII中Ueditor富文本编辑器文件和图片上传的配置图文教程
2017/03/15 PHP
JS在IE和FireFox之间常用函数的区别小结
2010/03/12 Javascript
解决ExtJS在chrome或火狐中正常显示在ie中不显示的浏览器兼容问题
2013/01/11 Javascript
动态添加option及createElement使用示例
2014/01/26 Javascript
javascript生成json数据简单示例分享
2014/02/14 Javascript
Javascript定义类(class)的三种方法详解
2015/03/13 Javascript
跟我学习javascript的执行上下文
2015/11/18 Javascript
AngularJS实现树形结构(ztree)菜单示例代码
2016/09/18 Javascript
ES6概念 Symbol.keyFor()方法
2016/12/25 Javascript
jQuery实现打开网页自动弹出遮罩层或点击弹出遮罩层功能示例
2017/10/19 jQuery
Vue-cli3项目配置Vue.config.js实战记录
2018/07/29 Javascript
jQuery插件实现非常实用的tab栏切换功能【案例】
2019/02/18 jQuery
JavaScript生成一个不重复的ID的方法示例
2019/09/16 Javascript
ES6常用小技巧总结【去重、交换、合并、反转、迭代、计算等】
2019/12/21 Javascript
python服务器端收发请求的实现代码
2014/09/29 Python
基于wxPython的GUI实现输入对话框(1)
2019/02/27 Python
使用Python做垃圾分类的原理及实例代码附源码
2019/07/02 Python
关于sys.stdout和print的区别详解
2019/12/05 Python
Python通过Tesseract库实现文字识别
2020/03/05 Python
pycharm的python_stubs问题
2020/04/08 Python
python将字典内容写入json文件的实例代码
2020/08/12 Python
Python Django路径配置实现过程解析
2020/11/05 Python
python 基于opencv 实现一个鼠标绘图小程序
2020/12/11 Python
找到不普通的东西:Bonanza
2016/10/20 全球购物
美国女性运动零售品牌:Lady Foot Locker
2017/05/12 全球购物
JSP和Servlet有哪些相同点和不同点,他们之间的联系是什么?
2015/10/22 面试题
小学生关于梦想的演讲稿
2014/08/22 职场文书
会计稽核岗位职责
2015/04/13 职场文书
2015年大学生工作总结
2015/04/21 职场文书
迎新生晚会主持词
2015/06/30 职场文书
学习焦裕禄先进事迹心得体会
2016/01/23 职场文书
JS轻量级函数式编程实现XDM二
2022/06/16 Javascript
windows10 家庭版下FTP服务器搭建教程
2022/08/05 Servers