承接上篇,已經把登入的畫面導向製作完成了,現在要進入核心的部分,也就是驗證啦!
先來準備一組對應的假資料,實際上不會那麼的...簡陋,token 基本是落落長且會更新,這邊先拿來應付範例使用:
const list = {
//username to token
Charlie: "0002",
Danny: "0005",
Hugh: "0008",
YT: "0015",
//token to username
"0002": "Charlie",
"0005": "Danny",
"0008": "Hugh",
"0015": "YT",
}
驗證有兩部分:
這一點其實 AuthProvider 已經建立好了,handleLogin
可以傳入一個參數,API回傳的資料可以藉由這邊傳入並儲存:
AuthProvider
const handleLogin = (data) => {
dispatch({ type: "LOGIN", payload: data })
onLogin?.(data)
}
因此登入頁面只要加上驗證的地方,也就是呼叫登入用的API login
:
LoginPage
import { useAuth } from 'react'
function LoginPage () {
const {state, handleLogin} = useAuth()
const [username, setUsername] = useState("")
const handleClick = async () => {
//這可以加入一些loading狀態切換
try {
const resData = await login(username) //會回傳 { token }
handleLogin(resData)
//由於導向已經在父層與Provider處理好了,這邊就不用再寫一次
} catch (error) {
console.log(error)
//這可以再加入額外的錯誤處裡
}
}
return <>
<Input onChange={e => setUsername(e.target.value)}/>
<Button onClick={handleClick}>LOGIN</Button>
</>
}
另外,要把 token 存在 localStorage,因此我們稍加增加一下 onLogin 的任務,也修改了一下原本的命名避免混肴:
Dashboard
function DashBoard (props) {
const handleAfterLogin = (data) => {
localStorage.setItem('token', data.token)
//to somewhere
}
const handleToLoginPage = () => {
//to login page
}
return <AuthProvider onLogin={handleLogin} onLogout={handleLogout}>
<Outlet/>
</AuthProvider>
}
handleAfterLogin 的 data 怎麼來:
/* 登入頁面的動作,呼叫了 AuthProvider 提供的 handleLogin */
const resData = await login(username)
handleLogin(resData)
/* AuthProvider 的 handleLogin 接受的資料會存起來,我們進一步將這個資料也傳入 onLogin */
const handleLogin = (data) => {
dispatch({ type: "LOGIN", payload: data })
onLogin?.(data)
}
/*
** handleAfterLogin(onLogin) 主要是負責登入的後續動作,
** 除了導向,透過傳入的 data 可以讓localStorage 保存 token
*/
const handleAfterLogin = (data) => {
localStorage.setItem('token', data.token)
//to somewhere
}
這樣的拆分與傳遞主要讓 AuthProvider 的任務專注在保存以及協助呼叫登入以及登出的動作,而開發者可以更彈性的來使用(不過你可能會更偏好把所有邏輯都囊括在Provider裡面,這邊單純以共用的角度來作拆分)
有了 token 之後,通常會有另一隻API GET /user/me
來知道當下登入者的資訊,以此範例我們有一個 getMe 來當作獲取資料的方式,我們可以把這個動作交給 AuthProvider
AuthProvider
function AuthProvider({
children,
onLogin,
onLogout,
validation, //可以傳入要驗證的動作,以此範例來說是 getMe
token, //會傳入給validation,以此範例會從 localStorage 取得
deps = [],
}) {
//前略
const [validating, setValidating] = useState(false) //提供給UI顯示狀態用
useEffect(() => {
const validate = async () => {
setValidating(true)
try {
const data = await validation?.(token)
dispatch({ type: "LOGIN", payload: { ...data, token } })
} catch (error) {
console.log(error.message)
handleLogout()
}
setValidating(false)
}
validate()
}, [state.isAuth, ...deps]) //使用 isAuth 來辨認需不需要再驗證
//後略
}
這樣處理了兩種情況:
這樣一來簡單的登入流程就完成啦!
可以看到登入後會有 Skeleton 閃來閃去,是藉由在 AuthProvider 提供 isPending 來進一步處理 (optional)
目前再次造訪的情境模擬會多打一次API(看起來卡卡的地方),先暫時忽略,屆時可以加入更多判斷來解決
看到現在,AuthProvider 不是 Hook 沒錯 (廢話 ( ゚Д゚)<!!
前一篇有講到說拿 AuthProvider 提供的 method 來進行登入登出,是使用 useAuth,而後續的頁面包含登出,使用者資訊等等也一樣可以透過 useAuth 取得,不過你也可以進一步拆分:
Provider
function Provider ({...}) {
//前略
const value = useMemo(
() => ({ state, actions: { handleLogin, handleLogout } }),
[state]
)
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
export const useMe = () => useContext(AuthContext).state
export const useAuth = () => useContext(AuthContext).actions
useMe
就是單純提供當前使用者資訊useAuth
就是單純提供登入登出的方法這一部分就是完全可以依照自己的情境來訂製需要的 hook。
登入好難。