这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天
Redux-toolkit 扩展 React 的状态管理,虽然市面上有着 Mobx 等等为流行的状态管理库,但是 Redux-toolkit 的功能十分完善,虽然使用起来非常的怪(和pinia那种管理模式完全不一样),但是当我们配置好之后就会发现其实很简单,而且维护起来也很容易。就是对NextJS支持不太好。
1. RTK 基本使用
1.1 Slice 切片
步骤:
-
引入
createSliceimport { createSlice } from "@reduxjs/toolkit" -
创建
slice 对象const xxxSlice = createSlice({/**/}) -
配置对象参数
const xxxSlice = createSlice({ name: '', initialState: {}, reducers: { setName(state, action) {}, setAddress(state, action) {} } })属性名 值类型 作用 name String 指定唯一标识 initialState any 设置切片的初始值 reducers Object 设置切片的方法 -
导出切片和对应的方法
export const { setName, setAddress } = xxxSlice.actions export const { reducer: xxxReducer } = xxxSlice -
例子:
// src/store/schoolSlice.js import { createSlice } from "@reduxjs/toolkit" const schoolSlice = createSlice({ name: 'school', initialState: { name: 'hdu', address: '白杨街道' }, reducers: { setName(state, action) { state.name = action.payload } } }) export const { setName, setAddress } = schoolSlice.actions export const { reducer: schoolReducer } = schoolSlice
1.2 配置以及使用切片
步骤:
-
引入
configureStoreimport { configureStore } from "@reduxjs/toolkit" -
引入切片
import { xxxReducer } from './xxxSlice' -
配置对象参数
const store = configureStore({ reducer: { xxx: xxxReducer } })属性名 类型 作用 reducer 对象 指定存储的切片 xxx 切片对象 为切片对象起名字 -
导出store
export default store -
最终效果
import { configureStore } from "@reduxjs/toolkit" import { schoolReducer } from "./schoolSlice" const store = configureStore({ reducer: { school: schoolReducer } }) export default store -
main.jsx使用切片6.1 引入
store和Provider标签import { Provider } from "react-redux" import store from './store/index'6.2 将
Provider标签作为APP的父标签,并指定 storeroot.render( <Provider store={store}> <App /> </Provider> )6.3 实例
// src/main.jsx import ReactDOM from "react-dom/client" import App from "./App" import { Provider } from "react-redux" import store from './store/index' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( <Provider store={store}> <App /> </Provider> ) -
组件中使用切片
7.1 导入
useDipatch、useSelector方法import { useDispatch, useSelector } from 'react-redux'7.2 导入切片中的方法
import { setName as setSchoolName, setAddress } from './store/school'7.3 使用
useSelector访问切片中的数据其中
state.school中的 school 是 步骤3 中指定的 xxxconst student = useSelector(state => state.school)7.4 使用
useDispatch访问切片中的方法const dispatch = useDispatch() const setNameHandler = () => { dispatch(setSchoolName('杭腚')) // 上下两个方法等效,其中 school 是在切片中指定的 name 属性 dispatch({ type: 'school/setName', payload: '杭腚' }) }console.log(setSchoolName('杭腚'))返回的数据7.5 实例
import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { setName as setSchoolName, setAddress } from './store/school' const App = () => { const school = useSelector(state => state.school) const dispatch = useDispatch() return ( <div> <p> {school.name} --- {school.address} </p> <button onClick={() => dispatch(setSchoolName('杭腚'))}>修改名字</button> <button onClick={() => dispatch(setAddress('地球'))}>修改地址</button> </div> ) } export default App
1.3 代码总结
-
创建 slice
// src/store/schoolSlice.js import { createSlice } from "@reduxjs/toolkit" const schoolSlice = createSlice({ name: 'school', initialState: { name: 'hdu', address: '白杨街道' }, reducers: { setName(state, action) { state.name = action.payload }, setAddress(state, action) { state.name = action.payload } } }) export const { setName, setAddress } = schoolSlice.actions export const { reducer: schoolReducer } = schoolSlice -
store 入口文件
// src/store/index.js import { configureStore } from "@reduxjs/toolkit" import { stuReducer } from "./stuSlice" import { schoolReducer } from "./school" const store = configureStore({ reducer: { school: schoolReducer } }) export default store -
main.jsx 入口文件
// src/main.jsx import ReactDOM from "react-dom/client" import App from "./App" import { Provider } from "react-redux" import store from './store/index' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( <Provider store={store}> <App /> </Provider> ) -
组件使用
// src/App.jsx import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { setName as setSchoolName, setAddress } from './store/school' const App = () => { const school = useSelector(state => state.school) const dispatch = useDispatch() return ( <div> <p> {school.name} --- {school.address} </p> <button onClick={() => dispatch(setSchoolName('杭腚'))}>修改名字</button> <button onClick={() => dispatch(setAddress('地球'))}>修改地址</button> </div> ) } export default App
2. RTKQ 基本使用
2.1 API 切片
步骤:
-
导入
createApi、fetchBaseQuery方法import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react' -
创建
API切片实例const studentApi = createApi({ reducerPath: 'studentApi', baseQuery: fetchBaseQuery({ baseUrl: 'http://127.0.0.1:3033/api/' }), tagTypes: ['student', 'teacher'], endpoints(build) { return { getStudents: build.query({ query() { return 'students' }, transformResponse(baseQueryReturnValue) { return baseQueryReturnValue.data }, providesTags: [{type: 'student', id: 'LIST'}] }) // ... } } }) -
createApi参数解析参数名 类型 作用 reducerPath string 指定唯一标识 baseQuery Function 一般指定 fetchBaseQuery 方法,指定请求根路径 tagTypes String[] 标签类型,标签用于方法之间的关联 endPoints Function 指定请求的一些方法 -
endpoints解析-
发起
get请求getXxx: build.query({ query() { return '子路径' } }) -
发起
post请求addXxx: build.mutation({ query() { return { url: '子路径', method: 'post', body: { data: 'xxx' } } } }) -
发起
put请求updateXxx: build.mutation({ query(id) { return { method: 'put', url: `子路径/${id}` } } }) -
参数
参数名 作用 transformResponse(baseQueryReturnValue) 指定返回的结果,例如完整的相应结果是 {status: ‘200’, data: [‘xj’, ‘sx’]},指定return baseQueryReturnValue.data后,返回的结果就是{data: [‘xj’, ‘sx’]}keepUnusedDataFor 指定缓存的时间,单位 s providesTags 指定某个方法具有的标签,当另一个方法使这个标签无效后,具有这个标签的方法就会触发 invalidatesTags 使某个或多个标签无效 如果想要更具体的指定某个数据,可以将
providesTags、invalidateTags变为对象或者函数形式getStudentsById: build.query({ query(id) { return `students/${id}` }, providesTags: (result, error, id) => ([{type: 'student', id: id}]) }) updateStudent: build.mutation({ query(stu) { return { url: `students${stu.id}`, method: 'put', body: { data: stu.attributes } } }, invalidatesTags: ((result, error, stu) => [ { type: 'student', id: stu.id }, { type: 'student', id: 'LIST' } ]) })
-
-
导出方法
注意命名规范
- get请求:
useXxxQuery - post、delete、put...请求:
useXxxMutation
export const { useGetStudentsQuery, useDelStudentMutation, } = studentApi export default studentApi - get请求:
2.2 组件中使用
const result = useGetStudentsQuery(null, {
// useQuery 可以接受一个对象作为第二个参数,通过该对象可以对请求进行配置
selectFromResult: result => { // 用来指定 useQuery 返回的结果
if(result.data) {
result.data = result.data.filter(item => item.attributes.age < 18)
}
return result
},
pollingInterval: 0, // 设置轮询的间隔,单位毫秒,0为不轮询
skip: false, // 设置是否跳过当前请求,默认false
refetchOnMountOrArgChange: false, // 设置是否每次都重新加载数据,false正常使用缓存,true每次都重新新加载数据,数字设置缓存的时间
refetchOnFocus: true, // 是否在重新获得焦点时加载数据,例如页面切换
refetchOnReconnect: false, // 是否在重新连接后重新加载数据(网又有了)
})
3. RTK 和 RTKQ 进阶使用
关键词:
3.1 项目结构
可以参考一下目录结构构建
3.2 代码使用
3.2.1 api 使用
有时候,当网页涉及到权限问题的时候,会将token存储到头部信息的 Authorizaiton 交付给服务器验证,这时候需要使用 prepareHeaders 为 endpoints 的每个方法都指定响应头。
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
headers.set('Authorization', `Bearer ${token}`)
return headers
}
其中 getState 可以获取 name 为 auth 的切片值,使用 headers.set() 方法即可指定头部信息。
完整代码:
其中可以直接写 export const authApi = createApi({/* CODE HERE */})
// auth.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'
export const authApi = createApi({
reducerPath: 'authApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://127.0.0.1:3033/api/',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
headers.set('Authorization', `Bearer ${token}`)
return headers
}
}),
endpoints(build) {
return {
register: build.mutation({
query(user) {
return {
url: 'auth/local/register',
method: 'post',
body: user, // username, password, email
}
},
}),
login: build.mutation({
query(user) {
console.log('user', user)
return {
url: 'auth/local',
method: 'post',
body: user // identifier
}
}
})
}
}
})
export const {
useRegisterMutation,
useLoginMutation
} = authApi
3.2.2 reducer 使用
initialState 也可以是是一个函数,更灵活地操作和指定返回值。
initialState: () => {
const token = localStorage.getItem('token')
if(!token) {
return /* CODE HERE */
}
return /* CODE HERE */
}
完整代码
import { createSlice } from "@reduxjs/toolkit";
export const authSlice = createSlice({
name: 'auth',
initialState: () => {
const token = localStorage.getItem('token')
if (!token) {
return {
isLogged: false,
token: '',
user: null,
expirationTime: 0
}
}
return {
isLogged: true,
token,
user: JSON.parse(localStorage.getItem('user')),
expirationTime: +localStorage.getItem('expirationTime')
}
},
reducers: {
login(state, action) {
state.isLogged = true
state.token = action.payload.token
state.user = action.payload.user
// 获取当前时间戳
const currentTime = Date.now()
// 设置登录的有效时间
const timeout = 1000 * 60 * 60 *24 * 7 // 一周
setTimeout.expirationTime = currentTime + timeout // 设置失效日期
localStorage.setItem('token', state.token)
localStorage.setItem('user', JSON.stringify(state.user))
localStorage.setItem('expirationTime', state.expirationTime + '')
},
logout(state, action) {
state.isLogged = false
state.token = ''
state.user = null
localStorage.removeItem('token')
localStorage.removeItem('user')
localStorage.removeItem('expirationTime')
}
}
})
export const { login, logout } = authSlice.actions
3.2.3 store 入口文件使用
其实配置的项很少,主要有一些坑,之前一直没注意到:
例如
[authApi.reducerPath]是一个表达式,在authApi文件中我们已经指定了reducerPath,所以应该是唯一的。configureStore中的reducer属性,配置的属性后面都有.reducer后缀,这样不管是Api还是Slice都好记一些middleware中间件配置,concat()函数可以用,分隔多个中间件
const store = configureStore({
reducer: {
[authApi.reducerPath]: authApi.reducer,
[studentApi.reducerPath]: studentApi.reducer,
auth: authSlice.reducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(
authApi.middleware,
studentApi.middleware
)
})
完整代码:
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import { authApi } from "./api/authApi";
import studentApi from "./api/studentApi";
import { authSlice } from "./reducer/authSlice";
const store = configureStore({
reducer: {
[authApi.reducerPath]: authApi.reducer,
[studentApi.reducerPath]: studentApi.reducer,
auth: authSlice.reducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(
authApi.middleware,
studentApi.middleware
)
})
setupListeners(store.dispatch)
export default store
3.2.4 组件中的使用
主要是忘记了,这里再写一点
-
如果是
Query ApiQuery Api钩子函数返回的是一个对象,其中data是服务器返回的值,isSuccess是返回是否成功。const { data, isSuccess } = useGetStudentsQuery() -
如果是
Mutation ApiMutation Api钩子函数返回的是一个数组,其中数组第一项是请求Api函数,即发起请求的函数,第二项是一个数组,有很多属性,依照名字看很好理解,不多介绍。const [regFn, { error: regError, isSuccess: regIsSuccess }] = useRegisterMutation()以
regFn为例,其返回值是一个Promise对象,可以对服务器返回的数据进行操作。loginFn({ identifier: username, password: password }).then(res => { if(!res.error) { // 登录成功后,需要向系统中添加一个标识,标记用户的登录状态 // 登录状态(布尔值,token(jwt)) // 跳转页面到根目录 dispatch(login({ token: res.data.jwt, user: res.data.user })) navigate('/form') } })刚突然发现之前上传的笔记是之前下载过的材料,还是没经过自己思考的那种(扇自己一个大嘴巴子),对以上代码建议创建代码模板,以后直接输入前缀就可以了,改动的地方其实很少的。