使用 Redux Toolkit 简化 Redux

386 阅读6分钟

本文章将讲解Redux并使用Redux Toolkit工具集简化Redux的使用;

Redux 是什么?

Redux是一个第三方的管理全局状态的工具库,所有的全局状态存储在一个可全局访问的Store当中,它可以使用action(动作)容易的去更新状态,让你更容易的理解状态如何被更新,跟Context API + useReducer的概念类似;

什么时候使用 Redux

许多应用都不需要使用Redux,除非你的应用当中:有大量的全局状态状态被频繁更新复杂的更新逻辑

Redux的 工作流程

事件处理通过dispatch(分发-触发一个事件)一个action(动作-发生了什么事)Store当中找到相应的Reducer(视为一个事件监听器)Reducer根据收到的action类型更新State,每个使用Store的组件将会重新渲染

Redux Toolkit 是什么?

Redux Toolkit是一个现代、高效使用Redux的开发工具集,简化了Redux的操作,它包括ImmerRedux-ThunkReselect三个核心包

  • Immer(处理不可变性),可以在reducer里直接改变状态
  • Redux-Thunk(中间件),处理异步操作
  • Reselect(Redux的简单'选择器'库),用来简化Reducer函数

还自带了一个DevTools工具方便调试代码,需在浏览器插件搜索Redux DevTools配合使用

使用 Redux Toolkit

安装工具包

npm install @reduxjs/toolkit react-redux

创建 Redux Store

src/store.js代码

configureStoreAPI 创建一个Redux储存实例,需要传入一个reducer参数(此时可以先忽略)

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'

const store = configureStore({
  reducer: {
    counter: counterReducer
  },
})

export default store

传入了一个{counter: counterReducer}对象时,表示在Redux状态对象状态中存在一个state.counter属性

为 React 提供 Redux Store

通过react-redux中的Provider组件将App组件包裹并将store作为prop传递

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import { Provider } from 'react-redux'
import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)

创建 Redux State Slice

目录规范

通常Redux会有一个规范的目录结构,/src/features/单个功能/功能对应的slice.js


创建一个 Slice 文件

Redux Toolkit中使用createSlice函数来创建一个slice(切片,管理特定的状态域),该函数需传递一个对象包含三个属性nameinitialStatereducers

  • name:一个字符串标识切片,将作用生成的 action types的前缀
  • initialStatereducer的初始值state对象
  • reducers:一个对象,包含多个处理action(动作)reducer函数

导入createSlice函数,并创建初始值对象

import { createSlice } from '@reduxjs/toolkit'

// 初始值
const initialState = {
  value: 0,
}

创建slice,在Redux Toolkit中可以编写直接改变State的逻辑,因为内部使用了ImmerRedux的状态更新都是以不可变的更新的方式不可变:复制原来的object/array,然后更新它的复制体

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value += 1
    },
    decrement(state) {
      state.value -= 1
    },
    incrementByAmount(state, action) {
      state.value += action.payload
    },
  },
})

导出对应的reducer以及action creater

// 每个 case reducers函数会生成对应的 action creater(返回action对象的函数)
export const { increment, decrement, incrementByAmount } = counterSlice.actions

// 导出相应的reducer
export default counterSlice.reducer

术语

slice文件是单个功能的reduceraction的合集

  • reducer是一个函数,接收当前的state和一个action对象,决定着如何更新状态,并返回新状态
  • action是一个对象{type:xx, payload: xx},用于描述动作(发生的事情)和传递的参数,action creator是一个创建并返回action对象的函数

在 React 组件中使用 Redux

React-Redux中有两个钩子,useSelectstore中获取数据,useDispatch分发对应的动作action

  • useSelector的参数是一个函数,函数的参数就是store存储的数据
  • useDispatch返回一个函数,在这个函数中需要传递一个action对象,触发slice中对应的reducer函数
import { useDispatch, useSelector } from 'react-redux'
import {
  increment,
  decrement,
  incrementByAmount,
  incrementAsync,
} from './counterSlice'
import { useState } from 'react'

const Counter = () => {
  const [number, setNumber] = useState(0)

  const counter = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
  return (
    <>
      <div>
        <button onClick={() => dispatch(increment())}>+</button>
        <span>{counter}</span>
        <button onClick={() => dispatch(decrement())}>-</button>
      </div>
      <div>
        <input
          type='number'
          value={number}
          onChange={(e) => setNumber(e.target.value)}
        />
        <button onClick={() => dispatch(incrementByAmount(+number))}>
          同步添加amount
        </button>
        <button onClick={() => dispatch(incrementAsync(+number))}>
          异步添加amount
        </button>
      </div>
    </>
  )
}

export default Counter

开发者工具栏中的 Redux

此前安装了浏览器插件Redux DevTools,按F12在工具栏中选择Redux,当dispatch分发的每个action都会被显示

可以看见分发dispatch(increment()),会发出一个action的对象即{type:counter/increment},因为Redux Tookits根据slice自动生成了对应的action(动作)typename + reducer组成;

payload 携带多个参数

reducer函数action是一个对象,其中action.payloaddispatch(action creater)携带的参数

dispatch(incrementByAmount(5))
// 这里的 action creator函数的参数 就是对应的action.payload
// 等价于 dispatch({type: 'counter/incrementByAmount', payload: 5}

使用action creator时只能传递一个参数,如果想传递多个参数

  1. 使用一个对象
dispatch(incrementByAmount({type: 'add', value: 5}))
incrementByAmount(state, action) {
  // 结构 action.payload 对象
  const {type, value} = action.payload
  if (type === 'add') {
    // state.value += value
  } else {
    // xxx
  }
},
  1. 自定义reducer函数,配置reducerprepare属性,prepare就是执行相应的reducer前执行的函数
dispatch(incrementByAmount('add', 5))
    incrementByAmount: {
      reducer(state, action) {
        const { type, value } = action.payload
        if (type === 'add') {
          state.value += value
        } else {
          state.value -= value
        }
      },
      prepare(type, value) {
        return {
          payload: {
            type,
            value,
          },
        }
      },
    },

使用 thunk 处理异步逻辑

Redux store只知道同步操作,而Redux Toolkits自带了thunk中间件,允许在dispatch分发action之后,执行对应的reducer函数之间进行异步操作;

编写一个thunk action creator,返回一个thunk函数,该函数有两个参数dispatchgetState,在thunk函数里进行异步处理

传统的方法

dispatch(incrementAsync(5))
export const incrementAsync = function (amount) {
  return function (dispatch, getState) {
    // 模拟异步操作
    setTimeout(() => dispatch(incrementByAmount(amount)), 1000)
  }
}

使用createAsyncThunkAPI自动生成thunk函数,第一个参数action.type的字符串,第二个参数是一个payload creator回调函数,会返回一个包含数据的Promise或者带有错误的Promise

export const incrementAsync = createAsyncThunk(
  'counter/incrementAsync',
  async () => {
    // 模拟异步请求
    try {
      const res = await new Promise((resolve, reject) => {
        setTimeout(() => resolve(5), 1000)
        // setTimeout(() => reject('error'), 1000)
      })
      return res
    } catch (error) {
      // 返回一个Promise.reject触发 reducer的rejected
      return Promise.reject(error)
    }
  }
)

需要在slice中的extraReducers中对三种动作类型进行处理,该函数有一个builder的参数,builder对象提供了一些方法可以定义额外的case reducer,并支持链式调用,addCase增加reducer,这些reducer可以对createAsyncThunk的状态进行监听处理对应的状态pendingfulfilledrejected

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    // 省略
  },
  extraReducers(builder) {
    builder
      .addCase(incrementAsync.pending, (state, action) => {
        console.log(1)
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        console.log(2, action)
        // state.value += action.payload
      })
      .addCase(incrementAsync.rejected, (state, action) => {
        console.log(3)
      })
  },
})

最后的想法

Redux是可以编写可预测、可测试的代码,Redux Toolkit极大的简化了Redux的操作,更容易的管理Redux的状态;

  • Redux适用于管理全局状态在大型应用中,当需要频繁更新和管理复杂状态时用它
  • Redux可以让逻辑UI进行分离,更容易的理解state在什么时候、什么地方,如何被更新
  • Redux Toolkit是更推荐编写Redux的工具集,支持thunk中间件、immer以可变的方式编写不可变状态,拓展的DevTools很容易看到对应的dispatch