Redux-toolkit使用 | 青训营笔记

354 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天

Redux-toolkit 扩展 React 的状态管理,虽然市面上有着 Mobx 等等为流行的状态管理库,但是 Redux-toolkit 的功能十分完善,虽然使用起来非常的怪(和pinia那种管理模式完全不一样),但是当我们配置好之后就会发现其实很简单,而且维护起来也很容易。就是对NextJS支持不太好。

1. RTK 基本使用

1.1 Slice 切片

步骤:

  1. 引入 createSlice

    import { createSlice } from "@reduxjs/toolkit"
    
  2. 创建 slice 对象

    const xxxSlice = createSlice({/**/})
    
  3. 配置对象参数

    const xxxSlice = createSlice({
      name: '',
      initialState: {},
      reducers: {
          setName(state, action) {},
          setAddress(state, action) {}
      } 
    })
    
    属性名值类型作用
    nameString指定唯一标识
    initialStateany设置切片的初始值
    reducersObject设置切片的方法
  4. 导出切片和对应的方法

    export const { setName, setAddress } = xxxSlice.actions
    export const { reducer: xxxReducer } = xxxSlice
    
  5. 例子:

    // 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 配置以及使用切片

步骤:

  1. 引入 configureStore

    import { configureStore } from "@reduxjs/toolkit"
    
  2. 引入切片

    import { xxxReducer } from './xxxSlice'
    
  3. 配置对象参数

    const store = configureStore({
        reducer: {
            xxx: xxxReducer
        }
    })
    
    属性名类型作用
    reducer对象指定存储的切片
    xxx切片对象为切片对象起名字
  4. 导出store

    export default store
    
  5. 最终效果

    import { configureStore } from "@reduxjs/toolkit"
    import { schoolReducer } from "./schoolSlice"const store = configureStore({
      reducer: {
        school: schoolReducer
      }
    })
    export default store
    
  6. main.jsx 使用切片

    6.1 引入 storeProvider 标签

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

    6.2Provider 标签作为 APP 的父标签,并指定 store

    root.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. 组件中使用切片

    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 中指定的 xxx

    const 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 代码总结

  1. 创建 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
    
  2. 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
    
  3. 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>
    )
    
  4. 组件使用

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

步骤:

  1. 导入 createApi、fetchBaseQuery 方法

    import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'
    
  2. 创建 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'}]
                })
                // ...
            }
        }
    })
    
  3. createApi 参数解析

    参数名类型作用
    reducerPathstring指定唯一标识
    baseQueryFunction一般指定 fetchBaseQuery 方法,指定请求根路径
    tagTypesString[]标签类型,标签用于方法之间的关联
    endPointsFunction指定请求的一些方法
  4. 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使某个或多个标签无效

      如果想要更具体的指定某个数据,可以将 providesTagsinvalidateTags 变为对象或者函数形式

      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' }
          ])
      })
      
  5. 导出方法

    注意命名规范

    • get请求: useXxxQuery
    • post、delete、put...请求: useXxxMutation
    export const {
      useGetStudentsQuery,
      useDelStudentMutation,
    } = studentApi
    export default studentApi
    

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 交付给服务器验证,这时候需要使用 prepareHeadersendpoints 的每个方法都指定响应头。

prepareHeaders: (headers, { getState }) => {
    const token = getState().auth.token
    headers.set('Authorization', `Bearer ${token}`)
    return headers
}

其中 getState 可以获取 nameauth 的切片值,使用 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 入口文件使用

其实配置的项很少,主要有一些坑,之前一直没注意到:

例如

  1. [authApi.reducerPath] 是一个表达式,在 authApi 文件中我们已经指定了 reducerPath,所以应该是唯一的。
  2. configureStore 中的 reducer 属性,配置的属性后面都有 .reducer 后缀,这样不管是 Api 还是 Slice 都好记一些
  3. 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 组件中的使用

主要是忘记了,这里再写一点

  1. 如果是 Query Api

    Query Api 钩子函数返回的是一个对象,其中 data 是服务器返回的值,isSuccess 是返回是否成功。

    const { data, isSuccess } = useGetStudentsQuery()
    
  2. 如果是 Mutation Api

    Mutation 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')
        }
    })
    

    刚突然发现之前上传的笔记是之前下载过的材料,还是没经过自己思考的那种(扇自己一个大嘴巴子),对以上代码建议创建代码模板,以后直接输入前缀就可以了,改动的地方其实很少的。