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

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

开始以前

阅读本文须要对如下几项有必定了解javascript

ECMAScript 6

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

Hooks

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

React 自己提供了一些 Hooks,好比 useState、useReducer 等。经过在一个以“use”做为命名起始的函数中调用这些 Hooks,就获得了一个 custom Hook(自定义 Hook)。react

Custom Hooks 容许咱们把任何逻辑封装到其中,以便于复用足够小的组件逻辑。正则表达式

Controlled Components

当咱们把像 <input> <textarea><select> 这样的 HTML 元素自己的状态交给 React state 去管理,咱们就获得了一个“受控组件”。数组

styled-components

一个与 React 契合良好的 CSS in JS 库。它容许你使用 JS 编写样式,并编译成纯 CSS 文件。函数

下面代码中全部的样式都是使用它编写的。若是对代码中样式的实现不是很感兴趣的话, 这个能够跳过。flex

代码实现

Input 组件

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

Input.jsspa

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 来初始化 valueerror 的值,分别保存用户输入的值和当前表单域的校验结果。

而后,声明一个 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 了,而且它也为我带来了很好的效果。

相关文章
相关标签/搜索