[分享] 實作註冊與登入頁面 (Formik + yup)

Y000ga
10 min readJan 10, 2023

--

Simple twitter 簡易社交平台Rent Helper 租屋小幫手 專案中,都有用到登入與註冊功能,在 Simple twitter 中是透過 Redux 將 validation 機制寫在 authInput-slice 裡面,並且透過使用三大要件 store/ action/ reducer 去實踐登入與註冊頁面時輸入的驗證過程。

// authInput-slice.jsx account 部分節錄

const initialState = {
account: {
content: '',
isValid: false,
message: '',
count: '',
},
}

const authInputSlice = createSlice({
name: 'authInput',
initialState: initialState,
reducers: {
accountAuth(state, action) {
if (
action.payload.trim().length < 4 ||
action.payload.trim().length > 30
) {
state.account.content = action.payload
state.account.isValid = false
state.account.message = '帳號請輸入 4 到 30 個字元的英文字母、數字!'
state.account.count = action.payload.trim().length
} else {
state.account.content = action.payload
state.account.isValid = true
state.account.message = ''
state.account.count = ''
}
},
})

把驗證機制與各種輸入值拉到 Redux 這個全域 Flux Library,其實讓我覺得很奇怪。畢竟表單輸入相關資料只應用在當下輸入的表單,只是為了讓註冊頁面和登入頁面都使用就把他們通通拉到全域變數等級。

根據 Redux 作者與 React 核心開發團隊之一的 Dan Abramov 針對 Question: How to choose between Redux’s store and React’s state? 一問題的回覆:

Use React for ephemeral state that doesn’t matter to the app globally and doesn’t mutate in complex ways.

For example, a toggle in some UI element, a form input state. Use Redux for state that matters globally or is mutated in complex ways. For example, cached users, or a post draft.

Sometimes you’ll want to move from Redux state to React state (when storing something in Redux gets awkward) or the other way around (when more components need to have access to some state that used to be local).

可見 Form input state 是完全不建議被放到 Redux store 裡面,因此我開始尋找其他可以用於表單管理與驗證的第三方套件。

在搜尋過程中,主要找到 React Hook Form 和 Formik (驗證則搭配 Yup),以下 npm trend 所做的統計與比較圖,formik 下載數多但 react-hook-form 下載數上升速度快。

Formik 與 Rect-hook-form 比較圖

基於刻意練習的精神,我決定在 Rent Helper 專案當中使用 Formik + yup。

以下節錄註冊表單進行解說:

Rent Helper 註冊頁面實際操作畫面
import { Formik, Form } from 'formik'
import * as yup from 'yup' // 用於撰寫表單驗證規則
import { Button } from '@mui/material'
import classes from './LoginForm.module.scss'
import { useNavigate } from 'react-router-dom'
import InputField from '../UI/InputField'
import { userSignupApi } from '../api/userApi'
import Swal from 'sweetalert2'

const SignupForm = () => {
const navigate = useNavigate()

// 針對每個輸入進行資料格式與數值的規則,只有符合這個規則才能 submit
const validate = yup.object({
name: yup
.string() // type 必須是字串
.trim() // 刪掉字串前後的空白後
.max(20, '使用者名稱長度不得大於 20') // 字數最大值 = 20 + Error Message
.required('使用者名稱欄位不得為空'), // 設定為必填欄位 + Error Message
account: yup
.string() // type 必須是字串
.trim() // 刪掉字串前後的空白後
.min(8, '帳號長度不得小於 8') // 字數最小值 = 8 + Error Message
.max(20, '帳號長度不得超過 20') // 字數最大值 = 20 + Error Message
.required('帳號欄位不得為空'), // 設定為必填欄位 + Error Message
password: yup
.string() // type 必須是字串
.trim() // 刪掉字串前後的空白後
.required('密碼欄位不得為空') // 設定為必填欄位 + Error Message
.matches(/[0-9]/, '密碼須包含一個數字') // 透過正規表達式控制必須包含一個數字
.matches(/[a-z]/, '密碼須包含一個小寫字母') // 透過正規表達式控制必須包含一個小寫字母
.matches(/[A-Z]/, '密碼須包含一個大寫字母') // 透過正規表達式控制必須包含一個大寫字母
.matches(/[^\w]/, '密碼須包含一個符號') // 透過正規表達式控制必須包含一個符號
.min(8, '密碼長度不得小於 8') // 字數最小值 = 8 + Error Message
.max(20, '密碼長度不得超過 20'), // 字數最大值 = 20 + Error Message
checkPassword: yup
.string() // type 必須是字串
.oneOf([yup.ref('password')], '與密碼不同')
// 透過直接操作 password DOM element 獲得 password 的值後,需要與其相符
.required('密碼確認欄位不得為空'), // 設定為必填欄位 + Error Message
})

const signupHandler = async (values) => {
// 這裡的 values 是一個 object,包含 account/ name/ password/ checkPassword
const res = await userSignupApi(values)
if (res.status === 200) {
const { token, user } = res.data
localStorage.setItem('token', token)
localStorage.setItem('userId', user.id)
await Swal.fire({
position: 'top-end',
icon: 'success',
title: '註冊成功',
showConfirmButton: false,
timer: 1500,
})
navigate('/search')
} else {
Swal.fire({
position: 'top-end',
icon: 'error',
title: `${res.data.message}`,
showConfirmButton: false,
timer: 1500,
})
}
}

return (
<Formik
initialValues={{ account: '', password: '', name: '', checkPassword: ''}}
validationSchema={validate} // 驗證規則
onSubmit={signupHandler} // 只有符合所有驗證規則,才能順利觸發 submit event
>
<Form className={classes.form}>
<div className={classes.title}>註冊</div>
<InputField
type='text'
placeholder='請輸入使用者名稱'
name='name' // 必填,需要和 yup.object 裡面設定的相同
label='使用者名稱'
/>
<InputField
type='text'
placeholder='請輸入帳號'
name='account'
label='帳號'
/>
<InputField
type='password'
placeholder='請輸入密碼'
name='password'
label='密碼'
/>
<InputField
type='password'
placeholder='請再次輸入密碼'
name='checkPassword'
label='密碼確認'
/>
<Button
type='submit' // 這裡要設定 type = 'submit' 才能順利觸發 submit event
variant='contained'
className={classes.button}
sx={{ m: 2, width: '80%' }}
>
註冊
</Button>
<div
className={classes.navigate}
onClick={() => {
navigate('/login')
}}
>
已經有帳號嗎? 登入
</div>
</Form>
</Formik>
)
}

export default SignupForm

--

--

Y000ga
Y000ga

Written by Y000ga

喜歡穿環、滑板、植物和閱讀的射手女子,在學習前端的路上邊走邊跳邊跌倒,人生清單超長一串,覺得挫折的時候就用力用力撸貓 ฅ•ω•ฅ