React 附帶了幾個內建的 Hook,例如useState、useContext和useEffect。有時,您會希望有一個 Hook 用於某些更具體的目的:例如,取得資料、追蹤使用者是否在線上或連接到聊天室。您可能在 React 中找不到這些 Hook,但您可以根據應用程式的需求建立自己的 Hook。 - 來源:React官方
Custom Hooks的使用情境:
重用邏輯
:
自定義 Hooks 讓我們將元件邏輯抽取出來,並在其他元件或項目中重複使用。
關注點分離
:
通過建立功能專一的 Hooks,我們可以將元件的內部邏輯分開,使之更具模組性且易於維護。
將相關聯的功能與邏輯打包在一個 Hook 中,使元件程式碼更加乾淨且易於理解。
基本用法:
須遵循以下命名約定:
hook名稱必須以以下字母開頭,
use
後面跟著大寫字母,例如useState(內建)
或useInput(自訂)。
更多custom hook的資訊請見React官方文件
先來回顧一下昨天的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
來調整表單驗證的邏輯,使程式碼更加簡潔也更好維護。
依照上面提及的規範,我們將這次要使用的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>
是 react-router-dom
提供的一個元素,允許使用者透過點擊或點擊導航到另一個頁面的元素。當使用者點擊一個 <Link>
元素時,URL 會改變,並且與那個 URL 匹配的元件將會被渲染出來。
to 屬性: to 屬性是 <Link>
元素的一個重要屬性,它指定了當連結被點擊時應該導航到的路徑。
<a>
標籤:當使用標準的 <a>
標籤並點擊連接時,會造成頁面全部刷新,導致程式需要重新加載並且重置狀態。<Link>
元素:使用 react-router-dom 中的<Link>
,導航到新的路由時,只會更改必要的內容而無需刷新整個頁面或重新加載程式,能保留狀態(如使用者登入資訊、主題設置等),這提供了更快且更順暢的使用者體驗。
我們將當前的程式
//Login.js
<button className="ml-2 duration-200 text-violet-600 cursor-pointer">
Sign Up
</button>
加上Link
tag,導航到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>
註冊頁的表單驗證其實也類似於登入頁,但程式碼比較多,所以這裡直接附上github連結
本次主要是想練習react的custom hook,所以沒有使用第三方套件。
在react的生態圈裡有常見以下兩種表單套件
在練習完基礎的表單驗證後,可以自行使用以上套件來進行改寫跟優化