使用 Hooks 簡化受控元件的狀態繫結

stockholm發表於2019-03-17

使用 Hooks 簡化受控元件的狀態繫結

開始之前

閱讀本文需要對以下幾項有一定了解

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 來初始化 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 了,並且它也為我帶來了很好的效果。

相關文章