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 高级语法介绍
Jun 15 Javascript
javascript Keycode对照表
Oct 24 Javascript
JS命名空间的另一种实现
Aug 09 Javascript
一个检测表单数据的JavaScript实例
Oct 31 Javascript
JS实现让网页背景图片斜向移动的方法
Feb 25 Javascript
信息页文内画中画广告js实现代码(文中加载广告方式)
Jan 03 Javascript
谈谈JavaScript类型系统之Math
Jan 06 Javascript
浅析Bootstrap组件之面板组件
May 04 Javascript
JS实现发送短信验证后按钮倒计时功能(防止刷新倒计时失效)
Jul 07 Javascript
解决微信二次分享不显示摘要和图片的问题
Aug 18 Javascript
解决IE11 vue +webpack 项目中数据更新后页面没有刷新的问题
Sep 25 Javascript
vue-cli4使用全局less文件中的变量配置操作
Oct 21 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版(3)
2006/10/09 PHP
PHP开发过程中常用函数收藏
2009/12/14 PHP
使用Huagepage和PGO来提升PHP7的执行性能
2015/11/30 PHP
PHP mysql事务问题实例分析
2016/01/18 PHP
php处理json格式数据经典案例总结
2016/05/19 PHP
javascript中函数作为参数调用的方法
2015/02/09 Javascript
基于BootStrap Metronic开发框架经验小结【六】对话框及提示框的处理和优化
2016/05/12 Javascript
vue webpack实用技巧总结
2018/04/24 Javascript
js replace 全局替换的操作方法
2018/06/12 Javascript
小程序扫描普通链接二维码跳转小程序指定界面方法
2019/05/07 Javascript
操作按钮悬浮固定在微信小程序底部的实现代码
2019/08/02 Javascript
微信小程序sessionid不一致问题解决
2019/08/30 Javascript
js实现贪吃蛇小游戏
2019/10/29 Javascript
vue.js路由mode配置之去掉url上默认的#方法
2019/11/01 Javascript
在vue中封装的弹窗组件使用队列模式实现方法
2020/07/23 Javascript
[48:12]Secret vs Optic Supermajor 胜者组 BO3 第三场 6.4
2018/06/05 DOTA
[35:34]Liquid vs Winstrike 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
Python中的getopt函数使用详解
2015/07/28 Python
Python2.x版本中基本的中文编码问题解决
2015/10/12 Python
python计算两个数的百分比方法
2018/06/29 Python
Python查找数组中数值和下标相等的元素示例【二分查找】
2019/02/13 Python
python的内存管理和垃圾回收机制详解
2019/05/18 Python
Python使用sklearn实现的各种回归算法示例
2019/07/04 Python
Pandas 解决dataframe的一列进行向下顺移问题
2019/12/27 Python
python 将列表里的字典元素合并为一个字典实例
2020/09/01 Python
用python 绘制茎叶图和复合饼图
2021/02/26 Python
如何使用css3实现一个类在线直播的队列动画的示例代码
2020/06/17 HTML / CSS
HTML5的自定义属性data-*详细介绍和JS操作实例
2014/04/10 HTML / CSS
美国婚礼和派对礼品网站:Kate Aspen(新娘送礼会、迎婴派对)
2018/03/28 全球购物
销售业务实习自我鉴定
2013/09/23 职场文书
团队激励口号
2014/06/06 职场文书
大学生党员自我剖析材料
2014/10/06 职场文书
工作证明格式范文
2015/06/15 职场文书
高中诗歌鉴赏教学反思
2016/02/16 职场文书
Java使用Unsafe类的示例详解
2021/09/25 Java/Android
MyBatis 动态SQL全面详解
2021/10/05 MySQL