第二十一課:Context Api教學實作與介紹part1
前一天我們完成了全端基本的Api串接,從一開始的前端UI設計,到nodejs Api建置,最後又回到我們的前端把Api辛辛苦苦的接上,在這套原則掌握後,我們的後台建置就可以很快速地依循概念進行,且都是接上介面的方式會越做越熟悉,今天要來進入新的觀念,認識Context Api
那在繼續往下到我們的hotelsListPage的資料串接前,我們必須先來搞定與搞懂ContextApi,來先搞動最重要的會員制,把首頁串接好登入、註冊等等功能。
contextApi的資料傳遞方式概念圖
上面的圖除了你可以發現,有了Context可以簡化很多事情,讓useState的資料傳遞路徑被可以縮短且使用變多元,或是Context的累計或是暫存state這邊動作,都與Api傳給後端資料庫的行為不同,前後端分離的結果可以讓網頁瀏覽變快,讓資料可以統一在一次回傳給資料庫等結果等等的。
那對開發react的人必定都不陌生上述提到的問題,並原本的解決方式為redux第三方套件,context Api也是近年來出現的,針對這兩者的擁護著,下列我整理了ContextApi與redux差異與優劣勢
了解後,回到我們的實作,這邊先來解析我們的login與register,一般實務面的註冊與登入,會是先註冊然後註冊成功後,跳轉到登入頁面,要你在把註冊好的帳號再登入一次,並登入後才會有登人成功,並在右上方顯示你登入的會員樣子等等的。所以釐清我們需要用到context的部分,只有登入後的會員資料顯示,並讓各個component需要時,可以自由抓取,(備註:這邊沒有使用useFetch,但其實應該要使用來顯示loading時的樣子,歡迎各位後來自己補上,並login會使用ContextApi來做loading)
所以既然要一次完成auth流程,我們將先來處理register page的啟用,讓用戶可以自行註冊,所以一樣我們要先來使用axios.post來抓取完整串/api/v1/auth/register
的Api,但這邊輸入/auth/register是因為一樣我們前面就做"proxy": "http://localhost:5000/api/v1"
代理了,所以不用再輸入/api/v1
const handleClick=async(e)=>{
e.preventDefault();
try{
const res = await axios.post("/auth/register", )
console.log(res)
}catch(error){
console.log(error)
}
}
上面負責處理註冊按鈕按下去後,送出註冊資料, e.preventDefault();這邊是防止onClick的其他submit效果,如送出我們的表單,我們只是要起動我們的axios.post 的啟動機制。 再來就是要處理我們的註冊資料,所以一樣我們使用useState()來紀錄我們"使用者姓名、帳號信箱....""等資料依序在input輸入完後紀錄進去,
const [error, setError] = useState("");
const [registerData, setRegisterData] = useState({
username: undefined,
email: undefined,
password: undefined
})
const handleChange = (e) => {
setRegisterData(prev => ({ ...prev, [e.target.id]: e.target.value }))
}
...prev,的...
,保留之前資料慢慢的,新增input進去useState,所以可以在還沒送出前,搜集好我們的註冊資料,所以這邊就可以把我們的handleClick的部分補上registerData,就可以完整送出
const res = await axios.post("/auth/register",registerData)
這邊我們還沒有利用到我們的input中的checkPassword,來確定說確認密碼輸入有沒有正確,與處理可能會回傳的註冊錯誤訊息,如"此使用者名稱已被使用",所以我們要去接我們的error回報,
handleClick內的try catch(error)
setError(error.response.data.message)
成功抓到錯誤訊息後,我們可以先來讓"此使用者名稱已被使用的"的紅字提醒與顯示,
{error && <span style={{color: "red"}}>{error}</span>}
然後這邊我們要再來應用error來幫我們加錯誤時會有的紅匡紅字,來確定輸入的密碼與再確定密碼是一致的
這邊是使用?: 來處理當error有出現並判讀他是什麼類型會跳出這些紅匡的警示,如果沒有就是原本的灰色實線1px厚度
<input type="text" id="username" placeholder='使用者姓名' onChange={handleChange}
style={error==="錯誤,此帳號或信箱已被註冊" ? {border:"2px solid red"}:{border:"1px solid grey"}} />
<input type="text" id="email" placeholder='帳號信箱' onChange={handleChange}
style={error==="錯誤,此帳號或信箱已被註冊" ? {border:"2px solid red"}:{border:"1px solid grey"}}/>
<input type='password' id="password" placeholder='新密碼' onChange={handleChange}
style={error==="密碼輸入不一樣" ? {border:"2px solid red"}:{border:"1px solid grey"}}/>
<input type='password' id="checkpassword" placeholder='確認密碼' onChange={handleCheckPassword}
style={error==="密碼輸入不一樣" ? {border:"2px solid red"}:{border:"1px solid grey"}}/>
並一樣幫checkPassword 的input設立useState然後寫handleCheckPassword把input的onChange值輸入進去,然後在用useEffect去比對,useEffect的啟動判斷dependency就是checkPassword 的state值轉變,並當這些情況發生會設立我們的Error訊息為密碼不一樣,然後div的判斷就會接到顯示紅匡紅字。
const [checkPassword,setPassword] =useState({
checkpassword:undefined,
})
useEffect(()=>{
if(checkPassword.checkpassword !== registerData.password)
{
setError("密碼輸入不一樣")
}else{
setError("")
}
},[checkPassword])
const handleCheckPassword=(e)=>{
setPassword(prev=>({...prev,[e.target.id]: e.target.value}))
}
然後最後註冊成功後,使她跳轉到login Page我們要進入loginPage的動畫顯示。
我們跟我們創建SearchBar時一樣,使用useNavigate來進跳轉分頁與夾帶我們想要的資料過去
const navigate = useNavigate()
與一樣在handleClick內加入
try {
const res = await axios.post("/auth/register", registerData)
//成功後跳轉去login 使用我們在header searchBar用過的useNavigate
navigate("/login",res)
//可以把資料也傳過去
}
完成後應該就會自動幫你帶到login了,login那邊就可以使用uselocation來接這邊的資料,這邊在我們就開始了解contextApi前,追求完美的人可以練習新增loading得情況,可以利用之前的fetchData的概念。
首先我們一樣先創建專門的context folder
useFetch 與 contextApi都算是useState的沿用,他們的共同點都會使用useEffect與都會有抓取資料的loading error狀態,但contextApi比useFetch多了可以存放的initial_state,useFetch主要是用來資料串接就沒了,所以像是register的postApi比較適合useFetch的模式,因為他不需要暫存註冊成功的結果,回傳註冊成功就好了,登入才比較需要使用contextApi,因為他要登入成功後讓瀏覽器的localStage順便計入登入的會員資料,所以使用上常常不知道怎麼用的話可以慢慢去頗析他們的差異,最後一起使用時就會越來越清楚用哪個就好了。
然後我們要來定義contextApi他自己的抓取行爲動作Reducer與dispatch,這邊真的是本作者覺得最難且最複雜的地方,主要是因為reducer密名不好記,造中文直翻可能會完全不太懂,但我們可以利用之前資料庫對照Api的方式來記這些名詞,主要來說reducer是為了設置我們對context的行為判斷依據,但他又不像有api需要以url傳遞,只需要告訴他什麼動作要更改什麼資料,以switch case完成這件事(if and elseif的條件子句來做依據),而dispatch就是在外面把我們定義好的動作做使用,如等等下面我們會用dispatch來與loginPage做搭配
這邊先設立我們的loginReducer,從實作中可能比較了解,我們這邊所有相關的動作都只是為了將用戶資料成功登入,並紀錄到我們的暫存器內,所以方法與useFetch一樣如下。
這邊我們都要再多做一步,設立一個新的constants資料夾,來放我們的變數,我們要把"start_login"這個設為新的變數,也就是條件子句的case 不要讓他用字串判讀,這邊其實是為了以後不會有bug出現而不知其原因,比如說dispatch要是type的內容打錯,會讓他抓不到我們的reducer但至多就這樣抓不到了,他不會跳出任何錯誤訊息,因為對他來說就是if()裡面的條件沒有人符合,所以沒抓到就沒反應,但這在debug的時候非常致命,因為不知道要怎麼找起錯誤點,所以要避免這種情況,我們會乾脆多累一點,把這些字串條件都宣告,這樣我們就是叫出這些宣告後的變數,如果有打錯,他就會顯示什麼、什麼undefined這樣,我們就知道我們哪裡打醋了。
設立變數的好處,容易能幫忙找出問題出在哪裡,字串出問題就什麼都不知道
export const start_login = "start_login";
export const login_success = "login_success";
export const login_failure = "login_failure";
export const logout = "logout";
目前完整的程式碼如下
import { createContext, useEffect, useReducer } from "react"
import { login_failure, login_success, logout, start_login } from "../constants/actionTypes";
const INITIAL_STATE = {
user: JSON.parse(localStorage.getItem("user")) || null,
loading: false,
error: null
}
export const LoginContext = createContext(INITIAL_STATE);
const LoginReducer = (state, action) => {
switch (action.type) {
case start_login:
return {
user: null,
loading: true,
error: null
};
case login_success:
return {
user: action.payload,
loading: false,
error: null
};
case login_failure:
return {
user: null,
loading: false,
error: action.payload
};
case logout:
return {
user: null,
loading: false,
error: null
};
default:
return state
}
}
現在搞懂上面都在幹嘛後,我們要繼續完成contextApi,要來設置useReducer與我們provider,provider函數用意就是告訴react說我們要啟用contextApi 然後用provider把我們的app.jsx包起來等於就是整個app都有一個額外的context暫存器可以使用。
由於localStorage存放的方式都是以json檔,但我們傳入的state都是延續useState的特質是object,所以如同Api傳接到後端,我們這邊也要進行轉檔,所以我們要把contextApi裡面的localStorage.setItem上傳時轉乘json檔,而getItem下來時轉回來objectJSON.stringify:object轉json
JSON.parse:json轉object
這邊為什麼stringify 字串化就是轉json,原因是json檔就是一種純文字檔,反之依然。
附上contextApi的內容
import { createContext, useEffect, useReducer } from "react"
import { login_failure, login_success, logout, start_login } from "../constants/actionTypes";
const INITIAL_STATE = {
user: JSON.parse(localStorage.getItem("user")) || null,
loading: false,
error: null
}
export const LoginContext = createContext(INITIAL_STATE);
const LoginReducer = (state, action) => {
switch (action.type) {
case start_login:
return {
user: null,
loading: true,
error: null
};
case login_success:
return {
user: action.payload,
loading: false,
error: null
};
case login_failure:
return {
user: null,
loading: false,
error: action.payload
};
case logout:
return {
user: null,
loading: false,
error: null
};
default:
return state
}
}
export const LoginContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(LoginReducer, INITIAL_STATE)
useEffect(() => {localStorage.setItem("user", JSON.stringify(state.user))
}, [state.user]
)
return (
<LoginContext.Provider
value={{
user: state.user,
loading: state.loading,
error: state.error,
dispatch,
}}>
{children}
</LoginContext.Provider>
)
}
今天專攻了解context Api概念為主,明天將會用它來串接大大小小的user資料,Context Api與redux等概念在一開始學習時會比較困難,但將context Api與useState的概念做延伸的話,就比較不會這麼難理解,並可以用它來實作各種你想要集結存放的state,所以之後可以用來放置使用者使用行為紀錄、購物車,等等類似外掛插件資料集的概念。