使用 Hooks 簡化受控元件的狀態繫結
React 在 16.8 版本中推出了 Hooks,它允許你在“函式元件”中使用“類元件”的一些特性。
React 本身提供了一些 Hooks,比如 useState、useReducer 等。通過在一個以“use”作為命名起始的函式中呼叫這些 Hooks,就得到了一個 custom Hook(自定義 Hook)。
Custom Hooks 允許我們把任何邏輯封裝到其中,以便於複用足夠小的元件邏輯。
當我們把像<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 validateTriggers
在函式體中,我們呼叫兩次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 了,並且它也為我帶來了很好的效果。