iT邦幫忙

2022 iThome 鐵人賽

DAY 21
1

Day21 自己做一個價值幾十萬的動態網站

第二十一課:Context Api教學實作與介紹part1

github Day21~23完成的連結

前一天我們完成了全端基本的Api串接,從一開始的前端UI設計,到nodejs Api建置,最後又回到我們的前端把Api辛辛苦苦的接上,在這套原則掌握後,我們的後台建置就可以很快速地依循概念進行,且都是接上介面的方式會越做越熟悉,今天要來進入新的觀念,認識Context Api

認識Context Api並創建會員制

那在繼續往下到我們的hotelsListPage的資料串接前,我們必須先來搞定與搞懂ContextApi,來先搞動最重要的會員制,把首頁串接好登入、註冊等等功能。

contextApi的資料傳遞方式概念圖

上面的圖除了你可以發現,有了Context可以簡化很多事情,讓useState的資料傳遞路徑被可以縮短且使用變多元,或是Context的累計或是暫存state這邊動作,都與Api傳給後端資料庫的行為不同,前後端分離的結果可以讓網頁瀏覽變快,讓資料可以統一在一次回傳給資料庫等結果等等的。

ContextApi與redux差異

那對開發react的人必定都不陌生上述提到的問題,並原本的解決方式為redux第三方套件,context Api也是近年來出現的,針對這兩者的擁護著,下列我整理了ContextApi與redux差異與優劣勢

串接login 與 register 頁面 LoginContextApi

了解後,回到我們的實作,這邊先來解析我們的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)

setError設置註冊等錯誤回報訊息

這邊我們還沒有利用到我們的input中的checkPassword,來確定說確認密碼輸入有沒有正確,與處理可能會回傳的註冊錯誤訊息,如"此使用者名稱已被使用",所以我們要去接我們的error回報,

handleClick內的try catch(error)

setError(error.response.data.message)

成功抓到錯誤訊息後,我們可以先來讓"此使用者名稱已被使用的"的紅字提醒與顯示,

 {error && <span style={{color: "red"}}>{error}</span>}

再確認密碼的設置與useEffect紅字紅匡的應用

然後這邊我們要再來應用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的概念。

創建LoginContextApi與其應用

首先我們一樣先創建專門的context folder

useFetch 與 contextApi都算是useState的沿用,他們的共同點都會使用useEffect與都會有抓取資料的loading error狀態,但contextApi比useFetch多了可以存放的initial_state,useFetch主要是用來資料串接就沒了,所以像是register的postApi比較適合useFetch的模式,因為他不需要暫存註冊成功的結果,回傳註冊成功就好了,登入才比較需要使用contextApi,因為他要登入成功後讓瀏覽器的localStage順便計入登入的會員資料,所以使用上常常不知道怎麼用的話可以慢慢去頗析他們的差異,最後一起使用時就會越來越清楚用哪個就好了。

了解Reducer在context的作用

然後我們要來定義contextApi他自己的抓取行爲動作Reducer與dispatch,這邊真的是本作者覺得最難且最複雜的地方,主要是因為reducer密名不好記,造中文直翻可能會完全不太懂,但我們可以利用之前資料庫對照Api的方式來記這些名詞,主要來說reducer是為了設置我們對context的行為判斷依據,但他又不像有api需要以url傳遞,只需要告訴他什麼動作要更改什麼資料,以switch case完成這件事(if and elseif的條件子句來做依據),而dispatch就是在外面把我們定義好的動作做使用,如等等下面我們會用dispatch來與loginPage做搭配

switch case解釋與設立constants用意

這邊先設立我們的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暫存器可以使用。

JSON.stringify & JSON.parse

由於localStorage存放的方式都是以json檔,但我們傳入的state都是延續useState的特質是object,所以如同Api傳接到後端,我們這邊也要進行轉檔,所以我們要把contextApi裡面的localStorage.setItem上傳時轉乘json檔,而getItem下來時轉回來object
JSON.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,所以之後可以用來放置使用者使用行為紀錄、購物車,等等類似外掛插件資料集的概念。


上一篇
「全端挑戰」提升使用者體驗,Skeleton Loading與@keyframes的應用與介紹
下一篇
「全端挑戰」dispatch與payload上傳、讀取操作,contextApi與localstorage的配合
系列文
自己做一個價值幾十萬的動態網站,學會Mern開發、前台UI設計各式觀念與各式Lib、typescript你該學會的前端技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言