2025-03-29 14:35:49 +08:00

21 KiB
Executable File
Raw Permalink Blame History

Redux

状态管理 适合大型项目 类似的还有zustand, mobx, mobx-react-lite

hello redux

npm install react-redux
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/redux/4.1.2/redux.min.js"></script>
    <div>
      <button id="sub">减少</button>
      <span id="count">1</span>
      <button id="add">增加</button>
    </div>
    <script>
      const subBtn = document.getElementById('sub')
      const addBtn = document.getElementById('add')
      const countSpan = document.getElementById('count')

      /** 网页中使用redux
       * 1. 引入redux包
       * 2. 创建reducer整合函数
       * 3. 通过reducer对象创建store
       * 4. 对store中的state进行订阅
       * 5. 通过dispatch派发state的操作指令
       * */

      /**
       * state 当前的state, 根据这个state生成新的state
       * action 是一个对象, 保存操作的信息
      */
      function reducer(state={count:0}, action){
        switch (action.type){
          case 'Add':
            return {...state, count:state.count +1}
          case 'Sub':
            return {...state, count:state.count -1}
          case 'Add_N':
            return {...state, count:state.count +action.payload}
          default:
            return state
        }
      }
      /**
       * 第一个参数是派发器,第二个参数是初始值(也可以在reducer设置初始值)
       */
      const store = Redux.createStore(reducer, {count:1})
      // 订阅, 其实就是回调, 发生变化的时候执行
      store.subscribe(()=>{
        // console.log(store.getState())
        countSpan.innerText = store.getState().count
      })

      let count = 1

      subBtn.addEventListener('click',()=>{
        // count--
        // countSpan.innerText = count
        store.dispatch({type: 'Sub'})
      })
      addBtn.addEventListener('click',()=>{
        // count++
        // countSpan.innerText = count
        store.dispatch({type: 'Add'})
      })
    </script>
  </body>
</html>

Redux Toolkit (RTK)

Quick Start | Redux Toolkit

问题:
1.如果state过干复杂将会非常难以维护
  - 可以通过对state 分组为解次这个向题创建多个reducer然后將其合并为一个
2.state每次操作时都需要对state进行复制然后再去修改
3.case后边的常量维护起来会比较麻烦
# 安装
npm install react-redux @reduxjs/toolkit -S
/**
 * createSlice:创建reducer(归纳函数)切片
 *    - 名字, 用来自动生成action.type
 *    - 初始值
 *    - reducers: 放更新state的方法
 *        - 方法有两个参数: state和action
 *          state是一个代理对象,可以直接修改
 *
 * 切片会自动生成actions, 打印actions可以发现存储多个函数:actionCreator() action创建器
 *
 * 调用action创建器后会自动创建action对象
 * action对象的结构 {type: 'name/函数名', payload: '函数的参数'}
 *
 * 创建store: configureStore(option)
 *     - {reducer: {名字:切片的reducer}}
 */

示例:

// @/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
// @/index.js

import { Provider } from 'react-redux';
import store from './store'

// ...
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)
//...
// 2.在组件中使用
// 通过useSelector()钩子获取state, 通过useDispatch()获取派发器对象
// 修改state的时候,派发器传入切片自动生成的action创建器
// action创建器里的值是要修改的值
// 当然也可以手动传如action:格式 {type: 'stu/stuName', payload: '猪八戒'},但不推荐,容易出错
// @/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 (
    <div className="App">
      <p>姓名: {stu.name}</p>
      <p>年龄: {stu.age}</p>
      <p>性别: {stu.gender}</p>
      <p>地址: {stu.address}</p>
      <p>
        <input type="text" value={inputName} onChange={e=>nameChangeHandler(e)}/>
        <button onClick={saveNameHandler}>修改名字</button>
      </p>
    </div>
  )
}

export default App

拆分模块

实际使用不会把切片都写在index里, 一般拆分开, 稍微修改下上面的代码

// 新建@/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
// @/store/index.js也要改下
import { configureStore } from '@reduxjs/toolkit'
import {stuReducer} from './stuSlice'

const store = configureStore({
  reducer: {
    student: stuReducer,
  },
})
export default store
// 引入的部分也要改下
// @/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

Q: query, 简化在 Web 应用程序中加载数据的常见情况

example

1 . 创建模块

// 创建rtkq中的api对象,会自动根据各种方法生成对应的钩子函数
// 参数:
//  - reducerPath: 标识
//  - baseQuery: 指定查询的基本信息, 发送请求使用的工具
//  - endpoints: 指定api的各种功能, build: 请求的构建器
//      - query 查询的方法,指定请求的路径
//      - mutation 更新的方法
//      - (可选)transformResponse 对响应数据进行处理

// 导出钩子命名规则: getStuList => useGetStuListQuery  use表示钩子,Query表示查询方法
// 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

// 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

// 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(
    <Provider store={store}>
      <App />
    </Provider>
)

4 . 使用 api

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 (
    <div className="App">
      {isLoading && <p>--数据正在加载--</p>}
      {isSuccess &&
        data.map(item=>{
          return <p key={item.id}>
            姓名: {item.attributes.name} | 
            性别: {item.attributes.gender==='male'?'男':'女'} | 
            年龄: {item.attributes.age} | 
            地址: {item.attributes.address}
          </p>
        })
      }
    </div>
  )
}

export default App

keepUnusedDataFor:缓存功能

rtkq会查询数据后会默认缓存60秒, 在期间不重复发请求而是使用缓存

设置缓存时间keepUnusedDataFor

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返回的对象

  /**
   * refetch: 用来重新加载数据的函数
   * status: 字符串,请求的状态
   * isFetching: boolean,是否正在加载
   * isLoading: boolean,数据是否第一次加载,调用refetch不会改变状态
   * isSuccess: boolean,请求是否成功
   * isError: boolean,是否出错
   * isUninitialized: boolean,是否还未开始发送
   * data: 最新的数据
   * currentData: 当前参数的最新数据,和data的区别:当参数发生变化的时候,最开始保存的是undefined
   */

useQuery传参

  // 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 . 创建模块

// build.mutation 更新的方法
// return的是一个对象,包括url, method, body
// 导出钩子同样有规则, use前缀表示钩子, Mutation表示更新
// 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

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

// 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(
    <Provider store={store}>
      <App />
    </Provider>
)

4 . 使用 api

// useMutation和useQuery不一样,返回的是数组,第一个是触发器,第二个是结果
// result的结构类似useQuery返回
// 触发器返回的res也是一个 promise对象
import {useDelStuByIdMutation} from '@/store/stuApi'

// ...

  const [delStu, {isLoading:delLoading}] = useDelStuByIdMutation()
  const delStuHandler = () => {delStu(id)} // id是从props中解构得到的

// ...

useMutation 返回的 result对象

/** 
* isError: boolean
* isLoading: boolean
* isSuccess: boolean
* isUninitialized: boolean
* originalArgs: 
* reset: fn
* status: string
*/

使数据失效: 数据标签

举例: 更新某个学生的信息, 保存成功后, 要让学生列表的组件重渲染

// 1. 设置有哪些标签类型 tagTypes
// 2. 需要更新的api功能设置 providesTags 提供标签
// 3. 触法更新的api功能设置 invalidatesTags 使标签失效
// 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'}]

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

设置请求头

// baseQuery 参数里, 除了 baseUrl,还有prepareHeaders用来设置请求头
// 第一个参数是默认headers, 第二个参数有获取state的方法
// 因为useSelect只能在函数组件或者自定义勾子中使用
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
  },
}),