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: '123123@qq.com', 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: '123123@qq.com', 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('111222333@qq.com')) } 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 函数,用来重新加载数据
使用中可以根据需要,选择要获取到的属性值。