React18 (五) RTK

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 函數,用來重新加載數據

使用中可以根據需要,選擇要獲取到的屬性值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章