iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
自我挑戰組

初探全端之旅: 以MERN技術建立個人部落格系列 第 21

[Day21] 使用Custom Hooks改寫表單驗證、註冊頁開發

  • 分享至 

  • xImage
  •  

大綱

  1. 什麼是Custom Hook
  2. 建立Custom hook
    • 建立custom hook - useInput
    • 調整登入頁表單邏輯
    • 導航到註冊頁
    • react-router-dom Link tag 介紹
  3. 註冊頁開發(範例code)
  4. 第三方表單套件介紹

1. 什麼是Custom Hook?

React 附帶了幾個內建的 Hook,例如useState、useContext和useEffect。有時,您會希望有一個 Hook 用於某些更具體的目的:例如,取得資料、追蹤使用者是否在線上或連接到聊天室。您可能在 React 中找不到這些 Hook,但您可以根據應用程式的需求建立自己的 Hook。 - 來源:React官方

Custom Hooks的使用情境:

  • 重用邏輯
    自定義 Hooks 讓我們將元件邏輯抽取出來,並在其他元件或項目中重複使用。

  • 關注點分離
    通過建立功能專一的 Hooks,我們可以將元件的內部邏輯分開,使之更具模組性且易於維護。
    將相關聯的功能與邏輯打包在一個 Hook 中,使元件程式碼更加乾淨且易於理解。

基本用法
須遵循以下命名約定:

hook名稱必須以以下字母開頭,use後面跟著大寫字母,例如useState(內建)或useInput(自訂)。

更多custom hook的資訊請見React官方文件

2. 建立Custom hook

先來回顧一下昨天的Login.js

import { useState } from "react";
import { Link } from "react-router-dom";
import Card from "../components/UI/Card"; //引入剛剛建立的Card

const LoginPage = (props) => {
  //email欄位值
  const [email, setEmail] = useState("");
  //email touched值
  const [emailTouched, setEmailTouched] = useState(false);

  //password欄位值
  const [password, setPassword] = useState("");
  //password touched欄位值
  const [passwordTouched, setPasswordTouched] = useState(false);
  
  //email的檢核
  const emailIsValid = email.trim() !== "";
  const emailInputIsInValid = !emailIsValid && emailTouched;
  
  //password的檢核
  const passwordIsValid = password.trim() !== "";
  const passwordInputIsInValid = !passwordIsValid && passwordTouched;

  //表單的狀態
  let formIsValid = false;

  if (emailIsValid && passwordIsValid) {
    formIsValid = true;
  }
  
  //當email input值變更時做的處理
  const handleEmailInputChange = (event) => {
    setEmailTouched(true);
    setEmail(event.target.value);
  };
  
  //當email input值blur時做的處理
  const handleEmailInputBlur = (event) => {
    setEmailTouched(true);
  };
  
  //當password input值變更時做的處理
  const handlePasswordInputChange = (event) => {
    setPassword(event.target.value);
    setPasswordTouched(true);
  };
  
  //當password input值blur時做的處理
  const handlePasswordInputBlur = (event) => {
    setPasswordTouched(true);
  };
  
  return (
    <Card>
      (略...)
    </Card>
  );
}
export default LoginPage;

可以看到有很多重複的邏輯(如上),比如帳號和密碼欄位都要監聽是否touched、是否有error等,今天要透過Custom hook來調整表單驗證的邏輯,使程式碼更加簡潔也更好維護。

建立useInput

依照上面提及的規範,我們將這次要使用的hook命名為useInput
先到hooks資料夾底下建立檔案useInput.js

//useInput.js

import { useState } from "react";

const useInput = (validateValue) =>{
    //設定input值
    const [enteredValue, setEnteredValue] = useState('');
    //設定input 是否被touched
    const [isTouched,setIsTouched] = useState(false);
    
    //透過傳入的validateValue(驗證函式)來檢驗當前輸入的值是否符合驗證,並取得對應的錯誤訊息
    const { isValid, errorMessage } = validateValue(enteredValue);
    
    //是否有錯誤
    const hasError = !valueIsValid && isTouched;

    //change事件處理
    const valueChangeHandler = (event) =>{
        setEnteredValue(event.target.value);
    }

    //blur事件處理
    const inputBlurHandler = (event) =>{
        setIsTouched(true);
    }

    //將以上的值return回去
    return{
        value: enteredValue,
        isValid,
        errorMessage,
        hasError,
        valueChangeHandler,
        inputBlurHandler
    }
}


export default useInput;

調整登入頁的驗證邏輯

回到Login.js

import { useState } from "react";
import api from "../api/api";
import Card from "../components/UI/Card";
import useInput from "../hooks/use-input";

const LoginPage = (props) => {

 const [errorMsg,setErrorMsg] = useState('');

 //Email驗證邏輯
 const validateEmail = (value) => {
    if (value.trim() === '') {
      return { isValid: false, errorMessage: 'Email is required.' };
    }
    //判斷email的格式是否合法
    const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
    if (!emailRegex.test(value)) {
      return { isValid: false, errorMessage: 'Please enter a valid email.' };
    }
    return { isValid: true, errorMessage: '' };
  };


  //使用custom input來處理email的欄位
  const {
    value: email,
    isValid: emailIsValid,
    errorMessage: emailErrorMessage,
    hasError: emailInputHasError,
    valueChangeHandler: emailChangeHandler,
    inputBlurHandler: emailBlurHandler,
  } = useInput(validateEmail);


  //使用custom input來處理password的欄位
  const {
    value: password,
    isValid: passwordIsValid,
    hasError: passwordInputHasError,
    valueChangeHandler: passwordChangeHandler,
    inputBlurHandler: passwordBlurHandler,
  } = useInput(value => value.trim() !== '');

  let formIsValid = false;

  if (emailIsValid && passwordIsValid) {
    formIsValid = true;
  }


  const handleSubmit = (event) => {
    if (!formIsValid) return;

    event.preventDefault();

    const userData = {
      email,
      password,
    };

    api
      .post("/auth/login", userData)
      .then((result) => {
        localStorage.setItem("user", JSON.stringify(result));
      })
      .catch((error) => {
        setErrorMsg('Login failed. Your email or password is incorrect.')
        console.error(error);
      });
  };

  const emailInputClasses = emailInputHasError
    ? "border-red-300 focus:ring-red-500"
    : "border-slate-300 focus:ring-sky-500";
  const passwordInputClasses = passwordInputHasError
    ? "border-red-300 focus:ring-red-500"
    : "border-slate-300 focus:ring-sky-500";

  return (
    <Card>
      <form className="w-full" onSubmit={handleSubmit}>
        <div className="mb-4">
          <label htmlFor="email" className="custom-font">
            Email
          </label>
          <input
            id="email"
            type="text"
            placeholder="請輸入Email"
            value={email}
            onBlur={emailBlurHandler}
            onChange={emailChangeHandler}
            className={`mt-1 block w-full px-3 py-2 bg-white border 
                                    rounded-md text-sm shadow-sm placeholder-slate-400
                                    focus:outline-none focus:ring-1  ${emailInputClasses}`}
          />
          {!emailInputHasError || (
            <p className="text-red-500 text-sm">Email is required</p>
          )}
        </div>
        <div>
          <label htmlFor="password" className="custom-font">
            Password
          </label>
          <input
            id="password"
            type="text"
            placeholder="請輸入密碼"
            value={password}
            onBlur={passwordBlurHandler}
            onChange={passwordChangeHandler}
            className={`mt-1 block w-full px-3 py-2 bg-white border 
                                    rounded-md text-sm shadow-sm placeholder-slate-400
                                    focus:outline-none focus:ring-1  ${passwordInputClasses}`}
          />
          {!passwordInputHasError || (
            <p className="text-red-500 text-sm">Password is required</p>
          )}
        </div>
        <button className="mt-8 px-4 py-2 bg-violet-600 hover:bg-violet-700  duration-200 text-white w-full rounded cursor-pointer">
          Sign In
        </button>
        {errorMsg && <p className="text-red-500 text-sm mt-2">{errorMsg}</p>}
        <div className="flex justify-center text-sm py-4">
            <p className="text-gray-400">Don't have an account?</p>
            <button className="ml-2 duration-200 text-violet-600 cursor-pointer">
              Sign Up
            </button>
        </div>
      </form>
    </Card>
  );
};

export default LoginPage;

導航到註冊頁

當我們透過點擊"Sign Up"文字我們希望要到Register page,此時就要透過react-router-dom的<Link>

Link tag 介紹

<Link>react-router-dom 提供的一個元素,允許使用者透過點擊或點擊導航到另一個頁面的元素。當使用者點擊一個 <Link> 元素時,URL 會改變,並且與那個 URL 匹配的元件將會被渲染出來。

to 屬性: to 屬性是 <Link> 元素的一個重要屬性,它指定了當連結被點擊時應該導航到的路徑。

Link的詳細資訊

Link tag 和 a tag的差別:

<a> 標籤:當使用標準的 <a> 標籤並點擊連接時,會造成頁面全部刷新,導致程式需要重新加載並且重置狀態。
<Link> 元素:使用 react-router-dom 中的<Link>,導航到新的路由時,只會更改必要的內容而無需刷新整個頁面或重新加載程式,能保留狀態(如使用者登入資訊、主題設置等),這提供了更快且更順暢的使用者體驗。

我們將當前的程式

//Login.js

  <button className="ml-2 duration-200 text-violet-600 cursor-pointer">
       Sign Up
  </button>
  

加上Linktag,導航到Register page

//Login.js
import { Link } from "react-router-dom";
...(略)
   <Link to="/register">
        <button className="ml-2 duration-200 text-violet-600 cursor-pointer">
          Sign Up
        </button>
   </Link>

3.註冊頁開發

註冊頁的表單驗證其實也類似於登入頁,但程式碼比較多,所以這裡直接附上github連結

4. 第三方表單套件介紹

本次主要是想練習react的custom hook,所以沒有使用第三方套件。

在react的生態圈裡有常見以下兩種表單套件

在練習完基礎的表單驗證後,可以自行使用以上套件來進行改寫跟優化

參考資料


上一篇
[Day20]登入頁開發和CORS處理
下一篇
[Day22] 前端Authentication處理
系列文
初探全端之旅: 以MERN技術建立個人部落格31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言