承接上篇,本篇要來實作簡單的登入功能。
期望登入是透過 JWT + localStorage 的方式來進行登入與保留登入的 token,並進一步搭配React Context + custom hook,那就來開始吧!
先按照計畫,把三個頁面都產生出來,如圖: (實際有點落差,因為只是畫個大概XD)
假設 /dashboard 是一個需要登入才能訪問(使用)的頁面,且預計會有上面三個頁面:
這三個畫面都會一隻獨立的檔案(或元件),如果是正在使用 react-router (v6) 的話,沒意外的話會是長這樣:
<Route path="dashboard/login" element={<Login />}/>
<Route path="dashboard/demo-1" element={<Demo1 />}/>
<Route path="dashboard/demo-2" element={<Demo2 />}/>
由於造訪這些頁面需要登入,因此這些頁面就會是 Context 要「包裹」的區域:
<AuthContext.Provider>
<Route path="dashboard/login" element={<Login />}/>
<Route path="dashboard/demo-1" element={<Demo1 />}/>
<Route path="dashboard/demo-2" element={<Demo2 />}/>
</AuthContext.Provider>
但實際上你無法這樣包裝,單純只是 react-router 無法這樣使用,因此可以進一步透過 Layour Route (Nested Route) 與 <Outlet/>
組合,實際大概會是這樣
// AuthContext.Provider 在這個元件裡面 ↓
<Route path="dashboard" element={<DashBoard/>}>
<Route path="login" element={<Login />}/>
<Route path="demo-1" element={<Demo1 />}/>
<Route path="demo-2" element={<Demo2 />}/>
</Route>
dashBoard.jsx
function DashBoard (props) {
return <AuthContext.Provider>
<Outlet/> //依照當前URL,對應的畫面就會渲染在 Outlet 這個位置,可以想像成 children
</AuthContext.Provider>
}
若是使用 Remix,雖然也是使用 react-router (畢竟都同一家人),則是依照資料夾結構來定義 (file-base routing),Remix 則是將 route 都定義在 app/routes/*
:
這時候的 /dashboard 畫面是空空如也,沒關係,先注重在其他路由上
const initValue = {isAuth: false}
const AuthContext = createContext(initValue)
function AuthProvider ({children, ...props}) {
return (
<AuthContext.Provider>
{children}
</AuthContext.Provider>
)
}
useReducer 負責保存相關資訊
{type, payload}
的格式,如果你有在使用 redux 應該就不陌生LOGIN
& LOGOUT
,dispatch 也亦同const authReducer = (state, action) => {
switch (action.type.toUpperCase()) { //單純避免typo
case "LOGIN": {
return { ...action.payload, isAuth: true }
}
case "LOGOUT": {
return { isAuth: false }
}
default: {
throw new Error("[AuthProvider] Action type is not exist.")
}
}
}
//in component ↓
const [state, dispatch] = useReducer(authReducer, initValue)
dispatch({ type: "LOGIN", payload: data })
dispatch({ type: "LOGOUT" })
如果這種格式覺得卡卡,你也可以再包裝成 (type, payload) => dispatch({type, payload})
接下來會進一步將 dispatch 使用在登入以及登出,先來安上看看。
<AuthProvider />
目前長這樣:
const AuthProvider() {
const [state, dispatch] = useReducer(authReducer, initValue)
return (
<AuthContext.Provider value={{state, dispatch}}>
{children}
</AuthContext.Provider>
)
}
把原本的 DashBoard 也修改一下:
function DashBoard (props) {
return <AuthProvider>
<Outlet/>
</AuthProvider>
}
如何在 Context 區域裡面使用到 state
& dispatch
呢?
其實簡單的一行即可:
const {state, dispatch} = useContext(AuthContext)
登入頁面就會長這樣:
import {useContext} from 'react'
import AuthContext from 'somewhere'
function LoginPage () {
const {state, dispatch} = useContext(AuthContext)
return <Button onClick={() => dispatch({type:"LOGIN"})}>LOGIN</Button>
}
目前這樣就是很單純切換登入的狀態(還沒有驗證),但通常成功登入後會進行一些額外的動作,我們把這個功能讓開發者可以自己傳入。
繼續拿原本的 Provider 開刀,現在整體會長這樣:
const AuthContext = createContext(initValue)
//略過reducer
export const AuthProvider({onLogin, onLogout}) {
const [state, dispatch] = useReducer(authReducer, initValue)
const handleLogin = (data) => {
dispatch({ type: "LOGIN", payload: data })
onLogin?.()
}
const handleLogout = () => {
dispatch({ type: "LOGOUT" })
onLogout?.()
}
return (
<AuthContext.Provider value={{state, handleLogin, handleLogout}}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => useContext(AuthContext)
我們把 dispatch 收回來,可以傳入兩個 callback 分別是 onLogin
& onLogout
並與 dispatch
包裝成 handleLogin
& handleLogout
。
當然,你也可以視情況加上 useCallback
因此剛剛在 DashBoard 套用的就可以傳入惹:
function DashBoard (props) {
const handleLogin = () => {
//to somewhere
}
const handleLogout = () => {
//to login page
}
return <AuthProvider onLogin={handleLogin} onLogout={handleLogout}>
<Outlet/>
</AuthProvider>
}
另外,我們把原本在登入頁面使用到 useContext 拉回來同一支檔案建立再 export 出去:
export const useAuth = () => useContext(AuthContext)
就不用每一次都要引入 useContext & Context,登入頁面現在就會長這樣:
// import { useContext } from 'react'
// import AuthContext from 'somewhere'
import { useAuth } from 'react'
function LoginPage () {
const {state, handleLogin} = useAuth()
return <Button onClick={handleLogin}>LOGIN</Button>
}
豪方便 (^・o・^)ノ”
其他的頁面也可以如法炮製,目前可以暢遊(?)頁面之間了
可以看到刷新頁面之後登入狀態也跟著不見,目前還沒儲存跟再驗證的機制
目前我們製作出了 AuthProvider 並可以傳入 onLogin/onLogout,透過 useAuth 的 hook 來處理登入登出的動作,至少頁面間的轉換沒問題了,接下來把驗證加入更完整一點吧!
待續