使用 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 了,而且它也爲我帶來了很好的效果。

相關文章
相關標籤/搜索