iT邦幫忙

2022 iThome 鐵人賽

DAY 17
0
Modern Web

React Hook 不求人,建立自己的 Hook Libary系列 第 17

[DAY 17] 自己的Hook自己做!AuthProvider 不是 Hook 吧?(上)

  • 分享至 

  • xImage
  •  

承接上篇,本篇要來實作簡單的登入功能。

期望登入是透過 JWT + localStorage 的方式來進行登入與保留登入的 token,並進一步搭配React Context + custom hook,那就來開始吧!

開始!

VIEW

先按照計畫,把三個頁面都產生出來,如圖: (實際有點落差,因為只是畫個大概XD)

假設 /dashboard 是一個需要登入才能訪問(使用)的頁面,且預計會有上面三個頁面:

  • /dashboard/login
  • /dashboard/demo-1
  • /dashboard/demo-2

這三個畫面都會一隻獨立的檔案(或元件),如果是正在使用 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 畫面是空空如也,沒關係,先注重在其他路由上

Context & Provider

  • 建立 Context & AuthProvider
const initValue = {isAuth: false}
const AuthContext = createContext(initValue)

function AuthProvider ({children, ...props}) {
  return (
    <AuthContext.Provider>
      {children}
    </AuthContext.Provider>
  )
}
  • useReducer 負責保存相關資訊

    • dispatch 使用了 {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})

useContext & 登入登出

接下來會進一步將 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>
}

目前這樣就是很單純切換登入的狀態(還沒有驗證),但通常成功登入後會進行一些額外的動作,我們把這個功能讓開發者可以自己傳入。

onLogin & onLogout

繼續拿原本的 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 來處理登入登出的動作,至少頁面間的轉換沒問題了,接下來把驗證加入更完整一點吧!

待續


上一篇
[DAY 16] 自己的Hook自己做!等等,請先登入!
下一篇
# [DAY 18] 自己的Hook自己做!AuthProvider 不是 Hook 吧?(下)
系列文
React Hook 不求人,建立自己的 Hook Libary30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言