1. Redux
Redux可以理解爲是reducer和context的結合體,使用Redux即可管理複雜的state,又可以在不同的組件間方便的共享傳遞state。當然,Redux主要使用場景依然是大型應用,大型應用中狀態比較複雜,如果只是使用reducer和context,開發起來並不是那麼的便利,此時一個有一個功能強大的狀態管理器就變得尤爲的重要。
狀態(State)
state直譯過來就是狀態,使用React這麼久了,對於state我們已經是非常的熟悉了。state不過就是一個變量,一個用來記錄(組件)狀態的變量。組件可以根據不同的狀態值切換爲不同的顯示,比如,用戶登錄和沒登錄看到頁面應該是不同的,那麼用戶的登錄與否就應該是一個狀態。再比如,數據加載與否,顯示的界面也應該不同,那麼數據本身就是一個狀態。換句話說,狀態控制了頁面的如何顯示。
但是需要注意的是,狀態並不是React中或其他類似框架中獨有的。所有的編程語言,都有狀態,所有的編程語言都會根據不同的狀態去執行不同的邏輯,這是一定的。所以狀態是什麼,狀態就是一個變量,用以記錄程序執行的情況。
容器(Container)
容器當然是用來裝東西的,狀態容器即用來存儲狀態的容器。狀態多了,自然需要一個東西來存儲,但是容器的功能卻不是僅僅能存儲狀態,它實則是一個狀態的管理器,除了存儲狀態外,它還可以用來對state進行查詢、修改等所有操作。(編程語言中容器幾乎都是這個意思,其作用無非就是對某個東西進行增刪改查)
可預測(Predictable)
可預測指我們在對state進行各種操作時,其結果是一定的。即以相同的順序對state執行相同的操作會得到相同的結果。簡單來說,Redux中對狀態所有的操作都封裝到了容器內部,外部只能通過調用容器提供的方法來操作state,而不能直接修改state。這就意味着外部對state的操作都被容器所限制,對state的操作都在容器的掌控之中,也就是可預測。a
總的來說,Redux是一個穩定、安全的狀態管理器。
2.RTK
除了Redux核心庫外Redux還爲我們提供了一種使用Redux的方式——Redux Toolkit。Redux工具包,簡稱RTK。RTK可以幫助我們處理使用Redux過程中的重複性工作,簡化Redux中的各種操作。
在React中使用RTK
安裝,無論是RTK還是Redux,在React中使用時react-redux都是必不可少,所以使用RTK依然需要安裝兩個包:react-redux和@reduxjs/toolkit。
npm
npm install react-redux @reduxjs/toolkit -S
yarn
yarn add react-redux @reduxjs/toolkit
RTK爲我們提供了一個configureStore方法,它直接接收一個對象作爲參數,可以將reducer的相關配置直接通過該對象傳遞,而不再需要單獨合併reducer。
如下面代碼樣例:
import { hanbaoReducer } from "./HanbaoSlice"; import { memberReducer } from "./MemberSlice"; //使用RTK構建store const { configureStore } = require("@reduxjs/toolkit"); const store = configureStore({ reducer: { member: memberReducer, hanbao: hanbaoReducer } }) export default store;
configureStore需要一個對象作爲參數,在這個對象中可以通過不同的屬性來對store進行設置,比如:reducer屬性用來設置store中關聯到的reducer,preloadedState用來指定state的初始值等,還有一些值我們會放到後邊講解。
reducer屬性可以直接傳遞一個reducer,也可以傳遞一個對象作爲值。如果只傳遞一個reducer,則意味着store中只有一個reducer。若傳遞一個對象作爲參數,對象的每個屬性都可以執行一個reducer,在方法內部它會自動對這些reducer進行合併。
RTK的API
CreateSlice
createSlice是一個全自動的創建reducer切片的方法,在它的內部調用就是createAction和createReducer。createSlice需要一個對象作爲參數,對象中通過不同的屬性來指定reducer的配置信息。
createSlice(configuration object)
配置對象中的屬性:
initialState —— state的初始值
name —— reducer的名字,會作爲action中type屬性的前綴,不要重複
reducers —— reducer的具體方法,需要一個對象作爲參數,可以以方法的形式添加reducer,RTK會自動生成action對象。
示例代碼如下:
//使用RTK構建store const { createSlice } = require("@reduxjs/toolkit"); const memberSlice = createSlice({ name: 'member',// 會自動生成action中的type initialState: { // init的state id: 1, username: 'kawa', email: '[email protected]', confirmed: true }, reducers: {//指定state的各種操作 setUsername(state, action) { // state是一個代理對象,可以直接修改 state.username = action.payload; }, setEmail(state, action) { state.email = action.payload; } } }) export const { setUsername, setEmail } = memberSlice.actions; export const { reducer: memberReducer } = memberSlice;
createSlice返回的並不是一個reducer對象而是一個slice對象(切片對象)。這個對象中我們需要使用的屬性現在有兩個一個叫做actions,一個叫做reducer。
Actions
切片對象會根據我們對象中的reducers方法來自動創建action對象,這些action對象會存儲到切片對象actions屬性中:
memberSlice.actions; // {setName: ƒ}
上例中,我們僅僅指定一個reducer,所以actions中只有一個方法setName,可以通過解構賦值獲取到切片中的action。
const {setEmail} = memberSlice.actions;
開發中可以將這些取出的action對象作爲組件向外部導出,導出其他組件就可以直接導入這些action,然後即可通過action來觸發reducer。
Reducer
切片的reducer屬性是切片根據我們傳遞的方法自動創建生成的reducer,需要將其作爲reducer傳遞進configureStore的配置對象中以使其生效:
const store = configureStore({
reducer: {
member: memberReducer,
hanbao: hanbaoReducer
}
})
總的來說,使用createSlice創建切片後,切片會自動根據配置對象生成action和reducer,action需要導出給調用處,調用處可以使用action作爲dispatch的參數觸發state的修改。reducer需要傳遞給configureStore以使其在倉庫中生效。
完整代碼:
index.js
import { hanbaoReducer } from "./HanbaoSlice"; import { memberReducer } from "./MemberSlice"; //使用RTK構建store const { configureStore } = require("@reduxjs/toolkit"); const store = configureStore({ reducer: { member: memberReducer, hanbao: hanbaoReducer } }) export default store;
MemberSlice.js
//使用RTK構建store const { createSlice } = require("@reduxjs/toolkit"); const memberSlice = createSlice({ name: 'member',// 會自動生成action中的type initialState: { // init的state id: 1, username: 'kawa', email: '[email protected]', confirmed: true }, reducers: {//指定state的各種操作 setUsername(state, action) { // state是一個代理對象,可以直接修改 state.username = action.payload; }, setEmail(state, action) { state.email = action.payload; } } }) export const { setUsername, setEmail } = memberSlice.actions; export const { reducer: memberReducer } = memberSlice;
HanbaoSlice.js
//使用RTK構建store const { createSlice } = require("@reduxjs/toolkit"); const hanbaoSlice = createSlice({ name:'hanbao', initialState:{ id: 1, title:'佛跳牆漢堡', price:88, desc:'美味', img: '//////' }, reducers: { setPrice(state,action){ state.price = action.payload; } } }) export const { setPrice } = hanbaoSlice.actions; export const { reducer: hanbaoReducer } = hanbaoSlice;
Home.js
import { useDispatch, useSelector } from "react-redux"; import * as React from 'react'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import { Button,LinearProgress } from "@mui/material"; import { setEmail } from "../../store/MemberSlice"; import { setPrice } from "../../store/HanbaoSlice"; function createData( id, username, email, confirmed ) { return { id, username, email, confirmed }; } export default function Home() { // useSelector用來加載state中的數據 const rct = useSelector(state => state); const member = rct.member; const hanbao = rct.hanbao; const rows = [ createData(member.id, member.username, member.email, member.confirmed), ]; const dispatch = useDispatch(); const changeEmail = () => { dispatch(setEmail('[email protected]')) } return ( <> <TableContainer component={Paper}> <Table sx={{ minWidth: 30 }} size="small" aria-label="a dense table"> <TableHead> <TableRow> <TableCell>ID</TableCell> <TableCell align="center">UserName</TableCell> <TableCell align="center">Email</TableCell> <TableCell align="center">Confirmed</TableCell> </TableRow> </TableHead> <TableBody> {rows.map((row) => ( <TableRow key={row.id}> <TableCell>{row.id}</TableCell> <TableCell align="center">{row.username}</TableCell> <TableCell align="center">{row.email}</TableCell> <TableCell align="center">{row.confirmed ? 'true' : 'false'}</TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> <Button variant="contained" onClick={changeEmail}>Update</Button> <LinearProgress /> {JSON.stringify(hanbao)} <Button variant="outlined" onClick={() => dispatch(setPrice(35))}>Update Price</Button> </> ); }
3.RTK query
RTK不僅幫助我們解決了state的問題,同時,它還爲我們提供了RTK Query用來幫助我們處理數據加載的問題。RTK Query是一個強大的數據獲取和緩存工具。在它的幫助下,Web應用中的加載變得十分簡單,它使我們不再需要自己編寫獲取數據和緩存數據的邏輯。Web應用中加載數據時需要處理的問題:
1. 根據不同的加載狀態顯示不同UI組件 2. 減少對相同數據重複發送請求 3. 使用樂觀更新,提升用戶體驗 4. 在用戶與UI交互時,管理緩存的生命週期
這些問題,RTKQ都可以幫助我們處理。首先,可以直接通過RTKQ向服務器發送請求加載數據,並且RTKQ會自動對數據進行緩存,避免重複發送不必要的請求。其次,RTKQ在發送請求時會根據請求不同的狀態返回不同的值,我們可以通過這些值來監視請求發送的過程並隨時中止。
創建Api切片
RTKQ中將一組相關功能統一封裝到一個Api對象中,比如下面關於Hanbao的api的功能封裝到hanbaoApi.js中
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; //創建API對象 const hanbaoApi = createApi({ reducerPath: 'hanbaoApi',//Api標識,不能和其他的Api或reducer重名 baseQuery: fetchBaseQuery({//指定查詢的基礎信息,發送請求使用的工具 baseUrl: 'http://localhost:1337/api/' }), tagTypes:['hanbao'], // 指定Api的標籤類型 endpoints(build) {// endpoints用來指定Api的各種功能, // build 構建請求的信息 return { getHanbaoList: build.query({ query() { return 'hanbaos' }, transformResponse(baseQueryReturnValue) { return baseQueryReturnValue.data; }, providesTags: ['hanbao'] // 緩存tag }), getHanbaoById: build.query({ query(id) { return `hanbaos/${id}`; }, transformResponse(baseQueryReturnValue) { return baseQueryReturnValue.data; }, keepUnusedDataFor: 5 //設置數據的緩存時間 單位秒,默認60秒 }), deleteHaobao: build.mutation({ query(id) { return { url: `hanbaos/${id}`, method: 'post' } }, invalidatesTags:['hanbao'] // 操作後失效的tag }) } } }) // Api對象創建後,對象中會根據各種方法生成對應的鉤子函數 // 通過這些鉤子函數可以向服務期發送請求,鉤子函數的命名規則 getHanbanList -> useGetHanbaoListQuery export const { useGetHanbaoListQuery, useGetHanbaoByIdQuery,useDeleteHaobaoMutation } = hanbaoApi; export default hanbaoApi;
上例是一個比較簡單的Api對象的例子,我們來分析一下,首先我們需要調用createApi()
來創建Api對象。createApi()
需要一個配置對象作爲參數,配置對象中的屬性繁多,我們暫時介紹案例中用到的屬性:
reducerPath
用來設置reducer的唯一標識,主要用來在創建store時指定action的type屬性,如果不指定默認爲api。
baseQuery
用來設置發送請求的工具,就是你是用什麼發請求,RTKQ爲我們提供了fetchBaseQuery作爲查詢工具,它對fetch進行了簡單的封裝,很方便,如果你不喜歡可以改用其他工具。
fetchBaseQuery
簡單封裝過的fetch調用後會返回一個封裝後的工具函數。需要一個配置對象作爲參數,baseUrl表示Api請求的基本路徑,指定後請求將會以該路徑爲基本路徑。配置對象中其他屬性暫不討論。
endpoints
Api對象封裝了一類功能,比如增刪改查,我們會統一封裝到一個對象中。一類功能中的每一個具體功能我們可以稱它是一個端點。endpoints用來對請求中的端點進行配置。
endpoints是一個回調函數,可以用普通方法的形式指定,也可以用箭頭函數。回調函數中會收到一個build對象,使用build對象對點進行映射。回調函數的返回值是一個對象,Api對象中的所有端點都要在該對象中進行配置。
對象中屬性名就是要實現的功能名,比如獲取所有學生可以命名爲getStudents,根據id獲取學生可以命名爲getStudentById。屬性值要通過build對象創建,分兩種情況:
查詢:build.query({})
增刪改:build.mutation({})
例如:
getHanbaoList: build.query({ query() { return 'hanbaos' }, transformResponse(baseQueryReturnValue) { return baseQueryReturnValue.data; }, providesTags: ['hanbao'] // 緩存tag }),
先說query,query也需要一個配置對象作爲參數。配置對象裏同樣有n多個屬性,現在直說一個,query方法。注意不要搞混兩個query,一個是build的query方法,一個是query方法配置對象中的屬性,這個方法需要返回一個子路徑,這個子路徑將會和baseUrl拼接爲一個完整的請求路徑。
上例中,我們創建一個Api對象hanbaoApi,並且在對象中定義了一個getHanbaoList方法用來查詢所有的hanbao信息。如果我們使用react下的createApi,則其創建的Api對象中會自動生成鉤子函數,鉤子函數名字爲useXxxQuery或useXxxMutation,中間的Xxx就是方法名,查詢方法的後綴爲Query,修改方法的後綴爲Mutation。所以上例中,Api對象中會自動生成一個名爲useGetStudentsQuery的鉤子,我們可以獲取並將鉤子向外部暴露。
export const {useGetHanbaoListQuery} = studentApi;
創建Store對象
Api對象的使用有兩種方式,一種是直接使用,一種是作爲store中的一個reducer使用。store是我們比較熟悉的,所以先從store入手。
import { setupListeners } from "@reduxjs/toolkit/dist/query"; import hanbaoApi from "./HanbaoApi"; //使用RTK構建store const { configureStore, getDefaultMiddleware } = require("@reduxjs/toolkit"); const store = configureStore({ reducer: { [hanbaoApi.reducerPath]:hanbaoApi.reducer }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(hanbaoApi.middleware) }) setupListeners(store.dispatch) // 設置以後將會支持, refetchOnFocus, refetchOnReconnect export default store;
創建store並沒有什麼特別,只是注意需要添加一箇中間件,這個中間件已自動生成了我們直接引入即可,中間件用來處理Api的緩存。store創建完畢同樣要設置Provider標籤,這裏不再展示。接下來,我們來看看如果通過hanbaoApi發送請求。由於我們已經將hanbaoApi中的鉤子函數向外部導出了,所以我們只需通過鉤子函數即可自動加載到所有的hanbao信息。比如在Home.js中的示例代碼:
import * as React from 'react'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import {CircularProgress } from "@mui/material"; import {useGetHanbaoListQuery } from "../../store/HanbaoApi"; import Hanbao from "./Hanbao/Hanbao"; function createData( id, username, email, confirmed ) { return { id, username, email, confirmed }; } export default function Home() { const { data, isSuccess, isLoading } = useGetHanbaoListQuery(null, { selectFromResult: result => { // 指定useQuery的返回結果,可以對返回結果二次加工 return result; }, pollingInterval:0,//設置輪訓的時間 單位毫秒 skip:false, //是否跳過當前請求, 默認false refetchOnMountOrArgChange:false, //設置是否每次的都加載數據, false使用緩存,true每次加載數據,數字緩存的時間 refetchOnFocus: true, // 是否在重新獲取焦點時重載數據 refetchOnReconnect: true //是否在重新連接後重載數據 }); return ( <> {isLoading && <CircularProgress />} {isSuccess && <TableContainer component={Paper}> <Table sx={{ minWidth: 30 }} size="small" aria-label="a dense table"> <TableHead> <TableRow> <TableCell>ID</TableCell> <TableCell align="center">Title</TableCell> <TableCell align="center">Price</TableCell> <TableCell align="center">Desc</TableCell> <TableCell align="center">Image</TableCell> <TableCell align="center">Action</TableCell> </TableRow> </TableHead> <TableBody> {data.map((row) => (<Hanbao key={row.id} row={row} />))} </TableBody> </Table> </TableContainer>} </> ); }
直接調用useGetHanbaoListQuery()它會自動向服務器發送請求加載數據,並返回一個對象。這個對象中包括了很多屬性:
data – 最新返回的數據
currentData – 當前的數據
error – 錯誤信息
isUninitialized – 如果爲true則表示查詢還沒開始
isLoading – 爲true時,表示請求正在第一次加載
isFetching 爲true時,表示請求正在加載
isSuccess 爲true時,表示請求發送成功
isError 爲true時,表示請求有錯誤
refetch 函數,用來重新加載數據
使用中可以根據需要,選擇要獲取到的屬性值。