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 函数,用来重新加载数据

使用中可以根据需要,选择要获取到的属性值。

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