# Redux
> 状态管理
> 适合大型项目
> 类似的还有zustand, mobx, mobx-react-lite
## hello redux
```sh
npm install react-redux
```
```html
Document
1
```
## Redux Toolkit (RTK)
[Quick Start | Redux Toolkit](https://redux-toolkit.js.org/tutorials/quick-start)
```
问题:
1.如果state过干复杂,将会非常难以维护
- 可以通过对state 分组为解次这个向题,创建多个reducer,然后將其合并为一个
2.state每次操作时,都需要对state进行复制,然后再去修改
3.case后边的常量维护起来会比较麻烦
```
```bash
# 安装
npm install react-redux @reduxjs/toolkit -S
```
```js
/**
* createSlice:创建reducer(归纳函数)切片
* - 名字, 用来自动生成action.type
* - 初始值
* - reducers: 放更新state的方法
* - 方法有两个参数: state和action
* state是一个代理对象,可以直接修改
*
* 切片会自动生成actions, 打印actions可以发现存储多个函数:actionCreator() action创建器
*
* 调用action创建器后会自动创建action对象
* action对象的结构 {type: 'name/函数名', payload: '函数的参数'}
*
* 创建store: configureStore(option)
* - {reducer: {名字:切片的reducer}}
*/
```
示例:
```js
// @/store/index.js
// 创建store
import { createSlice, configureStore } from '@reduxjs/toolkit'
// 初始值
const initialState = {
name: '唐僧',
age: 30,
gender: '男',
address: '长安',
}
// 创建切片, 切片会自动生成action和reducer
export const stuSlice = createSlice({
name: 'stu',
initialState,
reducers: {
setName: (state, action) => {
state.name = action.payload
},
setAge: (state, action) => {
state.age = action.payload
},
},
})
console.log(stuSlice.actions)
// actionCreator() action创建器
export const { setName, setAge } = stuSlice.actions
// 把action创建器暴露出去, 代替原来手写的{type,action}
const nameAction = setName('猪八戒')
console.log(nameAction)
// {type: 'stu/stuName', payload: '猪八戒'}
// 调用action创建器生成action对象
const store = configureStore({
reducer: {
student: stuSlice.reducer,
// reducer也是切片自动创建的
// 这里的student名字在引入store的时候要用到,切片可能有很多个,用名字取其中一个
},
})
console.log(store)
export default store
```
怎么使用:
```
// 1. 用redux提供的Provider把整个app包起来
// 和react自带的Provider的区别是这里传入的是store
```
```js
// @/index.js
import { Provider } from 'react-redux';
import store from './store'
// ...
root.render(
)
//...
```
```
// 2.在组件中使用
// 通过useSelector()钩子获取state, 通过useDispatch()获取派发器对象
// 修改state的时候,派发器传入切片自动生成的action创建器
// action创建器里的值是要修改的值
// 当然也可以手动传如action:格式 {type: 'stu/stuName', payload: '猪八戒'},但不推荐,容易出错
```
```js
// @/app.js
import { useSelector, useDispatch } from 'react-redux'
import {useState} from 'react'
import {setName} from './store'
import './App.css'
function App() {
// 通过useSelector()钩子获取state
// 默认state是所有的,可以只取一部分
const stu = useSelector(state => state.student)
// 这里的名字是configureStore参数里reducer对应的名字
// 通过useDispatch()获取派发器对象
const dispatch = useDispatch()
const [inputName, setInputName] = useState('')
const nameChangeHandler = (e)=>{
e.preventDefault()
setInputName(e.target.value)
}
const saveNameHandler = ()=>{
// dispatch传入切片自动生成的action
dispatch(setName(inputName))
}
return (
姓名: {stu.name}
年龄: {stu.age}
性别: {stu.gender}
地址: {stu.address}
nameChangeHandler(e)}/>
)
}
export default App
```
### 拆分模块
实际使用不会把切片都写在index里, 一般拆分开, 稍微修改下上面的代码
```js
// 新建@/store/stuSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
name: '唐僧',
age: 30,
gender: '男',
address: '长安',
}
export const stuSlice = createSlice({
name: 'stu',
initialState,
reducers: {
setName: (state, action) => {
state.name = action.payload
},
setAge: (state, action) => {
state.age = action.payload
},
},
})
export const { setName, setAge } = stuSlice.actions
export const {reducer:stuReducer} = stuSlice
```
```js
// @/store/index.js也要改下
import { configureStore } from '@reduxjs/toolkit'
import {stuReducer} from './stuSlice'
const store = configureStore({
reducer: {
student: stuReducer,
},
})
export default store
```
```js
// 引入的部分也要改下
// @/app.js
import { useSelector, useDispatch } from 'react-redux'
import {setName as setStuName} from './store/stuSlice'
// ...
const {student:stu} = useSelector(state => state)
const dispatch = useDispatch()
const saveNameHandler = ()=>{
// dispatch传入切片自动生成的action
dispatch(setName(inputName))
}
//...
```
## RTKQ
[RTK Query | Redux Toolkit](https://redux-toolkit.js.org/tutorials/rtk-query)
> Q: query, 简化在 Web 应用程序中加载数据的常见情况
### example
#### 1 . 创建模块
```js
// 创建rtkq中的api对象,会自动根据各种方法生成对应的钩子函数
// 参数:
// - reducerPath: 标识
// - baseQuery: 指定查询的基本信息, 发送请求使用的工具
// - endpoints: 指定api的各种功能, build: 请求的构建器
// - query 查询的方法,指定请求的路径
// - mutation 更新的方法
// - (可选)transformResponse 对响应数据进行处理
// 导出钩子命名规则: getStuList => useGetStuListQuery use表示钩子,Query表示查询方法
```
```js
// src/store/stuApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
endpoints: (build) => ({
getStuList: build.query({
query: () => 'students',
// transformResponse对响应数据进行处理
transformResponse(res){return res.data}
}),
getStuInfoById: build.query({
query: (id) => `students/${id}`, // 接收路径参数
}),
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
}),
}),
})
export const {useGetStuListQuery} = stuApi
```
#### 2 . 配置 store
```js
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import { stuApi } from './stuApi'
// 添加到store
export const store = configureStore({
reducer: {
// api对象的路径名当store的标识
// api对象的reducer自动生成的
[stuApi.reducerPath]: stuApi.reducer,
},
// 添加中间件用来实现缓存, 失效, 轮寻
// 必须添加
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(stuApi.middleware)
},
})
```
#### 3 . 注入 store
```jsx
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import './index.css'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
)
```
#### 4 . 使用 api
```jsx
import { useState } from 'react'
import './App.css'
import { useGetStuListQuery } from './store/stuApi'
function App() {
// 调用钩子查询数据
// 钩子返回一个对象,包括请求过程中相关的数据
const obj = useGetStuListQuery()
console.log(obj)
const { isLoading, isSuccess, data} = obj
return (
{isLoading &&
--数据正在加载--
}
{isSuccess &&
data.map(item=>{
return
姓名: {item.attributes.name} |
性别: {item.attributes.gender==='male'?'男':'女'} |
年龄: {item.attributes.age} |
地址: {item.attributes.address}
})
}
)
}
export default App
```
### `keepUnusedDataFor`:缓存功能
>rtkq会查询数据后会默认缓存60秒, 在期间不重复发请求而是使用缓存
设置缓存时间`keepUnusedDataFor`
```js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
keepUnusedDataFor: 5, // 单位是秒,0为不缓存
}),
}),
})
```
### useQuery返回的对象
```js
/**
* refetch: 用来重新加载数据的函数
* status: 字符串,请求的状态
* isFetching: boolean,是否正在加载
* isLoading: boolean,数据是否第一次加载,调用refetch不会改变状态
* isSuccess: boolean,请求是否成功
* isError: boolean,是否出错
* isUninitialized: boolean,是否还未开始发送
* data: 最新的数据
* currentData: 当前参数的最新数据,和data的区别:当参数发生变化的时候,最开始保存的是undefined
*/
```
### useQuery传参
```js
// useQuery可以接收一个对象作为第二个参数,对请求进行配置
const { isFetching, isSuccess,isError, data } = useGetStuInfoByIdQuery(props.id, {
selectFromResult: result => result, // 对返回进行处理
pollingInterval: 0, // 轮寻间隔,单位毫秒.0表示不轮寻
skip:!props.id, // 是否跳过,默认false
refetchOnMountOrArgChange: false, // 设置是否每次组件挂载都重新加载数据,也可以传时间,单位是秒.例如设置2,表示两秒内,不重新加载
refetchOnFocus: false, // 页面获取焦点时是否重新加载数据,例如切换浏览器标签
refetchOnReconnect: false, // 网络恢复连接重新加载数据
// refetchOnFocus和refetchOnReconnect 需要额外在'@/store/index.js'里添加
// 1. import { setupListeners } from '@reduxjs/toolkit/query'
// 2. setupListeners(store.dispatch)
})
```
### useMutation
#### 1 . 创建模块
```js
// build.mutation 更新的方法
// return的是一个对象,包括url, method, body
// 导出钩子同样有规则, use前缀表示钩子, Mutation表示更新
```
```js
// src/store/stuApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
endpoints: build => ({
// 删除学生信息
delStuById: build.mutation({
query: id => {
// 如果发送的不是get请求,要返回一个对象,并配置信息
return {
url: `students/${id}`,
method: 'delete',
}
},
}),
// 添加学生
addStu: build.mutation({
query: stu => {
// post请求包含body属性
return {
url: 'students',
method: 'post',
body: { data: stu },
}
},
}),
// 更新学生信息
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
}),
}),
})
export const {
useDelStuByIdMutation,
useAddStuMutation,
useUpdateStuByIdMutation,
} = stuApi
```
#### 2 . 配置 store
```jsx
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { stuApi } from './stuApi'
// 添加到store
const store = configureStore({
reducer: {
// api对象的路径名当store的标识
// api对象的reducer自动生成的
[stuApi.reducerPath]: stuApi.reducer,
},
// 添加中间件用来实现缓存, 失效, 轮寻
// 必须添加
// 多个中间件, contat(a,b,c)
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(stuApi.middleware)
},
})
setupListeners(store.dispatch)
export default store
```
#### 3 . 注入 store
```jsx
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import './index.css'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
)
```
#### 4 . 使用 api
```js
// useMutation和useQuery不一样,返回的是数组,第一个是触发器,第二个是结果
// result的结构类似useQuery返回
// 触发器返回的res也是一个 promise对象
```
```jsx
import {useDelStuByIdMutation} from '@/store/stuApi'
// ...
const [delStu, {isLoading:delLoading}] = useDelStuByIdMutation()
const delStuHandler = () => {delStu(id)} // id是从props中解构得到的
// ...
```
### useMutation 返回的 result对象
```js
/**
* isError: boolean
* isLoading: boolean
* isSuccess: boolean
* isUninitialized: boolean
* originalArgs:
* reset: fn
* status: string
*/
```
### 使数据失效: 数据标签
> 举例: 更新某个学生的信息, 保存成功后, 要让学生列表的组件重渲染
```js
// 1. 设置有哪些标签类型 tagTypes
// 2. 需要更新的api功能设置 providesTags 提供标签
// 3. 触法更新的api功能设置 invalidatesTags 使标签失效
```
```js
// src/store/stuApi.js
/**
* 1. getStuList的标签是'stuList', addStu可以让所有的'stuList'标签失效,从而触发getStuList重新加载数据, 组件重渲染
* 标签可以有多个
* 2. 需要更颗粒化控制, 例如id为1的学生更新了信息, 只需要下次查询id为1的学生不使用缓存, 其他id如果有缓存就使用缓存
* providesTags属性设置为函数,函数返回一个数组,数组里放对象,对象包括type属性和id,id来自query传参的id
* invalidatesTags同样属性设置为函数,函数返回一个数组,数组里放对象,对象包括type属性和id,id来自query传参的id
**/
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
tagTypes: ['stuList','stuInfo'], // 指定api中的标签类型--type
endpoints: build => ({
// 获取学生列表
getStuList: build.query({
query: () => 'students',
// transformResponse对响应数据进行处理
transformResponse(res) {
return res.data
},
providesTags: ['stuList'] // 当标签失效的时候,触发数据加载. 这里的标签可以有多个,其中一个失效,就重新获取
}),
// 根据id获取学生信息
getStuInfoById: build.query({
query: id => `students/${id}`,
transformResponse(res) {
return res.data.attributes
},
keepUnusedDataFor: 30, // 设置缓存时间,单位秒,0是不缓存
providesTags: (result,error,id)=>{
// 设置tag更加详细,可以让某个type+id的失效,而不是整个type
// result: 请求返回的信息
// error: 错误信息
// id: query接收的id
return [{type: 'stuInfo',id}]
}
}),
// 删除学生信息
delStuById: build.mutation({
query: id => {
// 如果发送的不是get请求,要返回一个对象,并配置信息
return {
url: `students/${id}`,
method: 'delete',
}
},
}),
// 添加学生
addStu: build.mutation({
query: stu => {
return {
url: 'students',
method: 'post',
body: { data: stu },
}
},
invalidatesTags: ['stuList'] // 使标签失效
}),
// 更新学生信息
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
invalidatesTags: (result,error,id)=>{
return ['stuList',{type:'stuInfo',id}] // 类似providesTags
}
}),
}),
})
export const {
useGetStuListQuery,
useGetStuInfoByIdQuery,
useDelStuByIdMutation,
useAddStuMutation,
useUpdateStuByIdMutation,
} = stuApi
```
上面的`tagTypes`可以简化成一个, 学生列表没有id, 可以给它指定一个独有的id, 如`[{type:'student',id:'list'}]`
```js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
tagTypes: ['student'], // type改成只有一个
endpoints: build => ({
getStuList: build.query({
query: () => 'students',
transformResponse(res) {
return res.data
},
providesTags: [{type:'student',id:'list'}] // 指定id为'list'
}),
// 根据id获取学生信息
getStuInfoById: build.query({
query: id => `students/${id}`,
transformResponse(res) {
return res.data.attributes
},
keepUnusedDataFor: 30,
providesTags: (result,error,id)=>{
return [{type: 'student',id}]
}
}),
// 删除学生信息
delStuById: build.mutation({
query: id => {
return {
url: `students/${id}`,
method: 'delete',
}
},
invalidatesTags: [{type:'student',id:'list'}] // 使{type:'student',id:'list'}标签失效
}),
// 添加学生
addStu: build.mutation({
query: stu => {
return {
url: 'students',
method: 'post',
body: { data: stu },
}
},
invalidatesTags: [{type:'student',id:'list'}] // 使标签失效
}),
// 更新学生信息
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
invalidatesTags: (result,error,id)=>{
return [{type:'student',id:'list'},{type:'student',id}] // 使多个标签失效
}
}),
}),
})
export const {
useGetStuListQuery,
useGetStuInfoByIdQuery,
useDelStuByIdMutation,
useAddStuMutation,
useUpdateStuByIdMutation,
} = stuApi
```
### 设置请求头
```js
// baseQuery 参数里, 除了 baseUrl,还有prepareHeaders用来设置请求头
// 第一个参数是默认headers, 第二个参数有获取state的方法
// 因为useSelect只能在函数组件或者自定义勾子中使用
```
```js
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:1337/api/',
prepareHeaders: (headers, { getState }) => {
// 获取token
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
},
}),
```