组件再多也不怕!📦Redux+Redux Toolkit概念介绍与实战使用

2,543 阅读12分钟

前言

首先我原本是一个vue开发者,会有vuex的使用经验,虽然说可能上手会快,但是也有可能会被框架包袱所限制 为了写好今天这篇,看了特别多文档。

首先吐槽一下,最开始的时候我看的是这个文档 redux中文文档 我还想着这个中文文档真的是给人类看的吗?翻译成这样我还不如去看英文文档。每个字我都认识,连起来就看不懂了。

后面才知道上面那个不是官网,这个 redux中文官网 才是正版。针不戳,刚开始就踩坑。

今天是学习react的第七天,我的第一个小目标是将平时使用的todo清单软件通过react在web端一比一的实现所有功能!

📦代码仓库链接 react-todo gitee仓库

💻在线预览效果 react-todo 开发进度

# 👀从零开始学React第一天~React基础框架的构建(Create React App+Tailwind css+Material ui)

# 👀从零开始学React第二天~React配置Eslint+路由导航的实现(react-router-dom)

# 👀从零开始学React第三天~React日期选择器组件开发+Dayjs的使用

# 👀从零开始学React第四天~📆实现一个好看的弹窗日历组件

# 👀从零开始学React第五天~📈实现hook相关性能优化

Redux 介绍

官方介绍如下:

Redux 是一个使用叫做“action”的事件来管理和更新应用状态的模式和工具库 它以集中式Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

我的理解是你可以将你项目中需要提供给多个组件使用的变量给抽离出来,放在一个仓库中单独维护。

举个场景例子,如果你创建了一个很多变量的👴爷爷组件,它的👶孙子组件需要使用其中的几个变量,但是它的👦儿子组件不用,但是如果你想要传递这个变量就必须 修改下儿子组件 通过它来传递。

又或者 很多变量的爷爷组件 需要新增一个兄弟 👨‍🦳二爷爷组件,它需要用到爷爷组件的变量,这时候我们就只能把这个变量放到爷爷和二爷爷的父亲组件 👼太爷爷组件 那里去了。

如果你的整个项目多个组件需要共享和使用相同变量,那么你就只能全部放在 🐵族谱里年龄最长的那个组件中

而redux的做法是将你项目中的变量单独放在一个位置管理,把整个 组件族谱的变量统一放到一个分好类的仓库 里,谁要哪个值直接去拿,多方便~

后面的内容都会以上例子以及 我的TODO清单项目 为基准进行拓展,更加形象的来讲解 Redux,安装 redux 执行下面的命令噢

Redux Toolkit 是react官方开箱即用的高效 Redux 开发工具集,下面的案例都是以这个库中的api进行开发的。

概念与使用

接下来我们边开发边讲概念,这样才能 理论与实践并重,记得搭配目录食用!

上面一直说的都是 变量 这个词,事实上应该叫做 state 也就是数据的状态,只是为了好理解这么说的哈~ 后面的概念中会频繁出现滴~

configureStore():创建仓库

首先我们肯定是需要有一个创建仓库的方法,在redux中使用的是 configureStore(), 在index.js中写入如下代码

官方介绍: Redux store 是使用 Redux Toolkit 中的 configureStore 函数创建的。configureStore 要求我们传入一个 reducer 参数。

//index.js
import { configureStore } from "@reduxjs/toolkit"
export default configureStore({
    reducer: {
        // 这里放入各个模块
    },
})

仓库创建好了,我们要怎么往里面放东西呢?首先要理解我们肯定不能把东西在仓库里乱堆,要是乱堆的话和放在🐵最高层组件上有啥区别呢?

我们要做的是给仓库分类存放,而分类中每一个类在rudex中就是切片的概念。

Slice:仓库分类

Slice Reducer切片 其实就是类似于vuex中的 module模块,也就是把仓库一格格的分开,方便放也方便找!

切片”是应用中单个功能的 Redux reducer 逻辑和 action 的集合, 通常一起定义在一个文件中。该名称来自于将根 Redux 状态对象拆分为多个状态“切片”。

创建切片的方法是 createSlice(),案例如下:

import { createSlice } from "@reduxjs/toolkit"

export const slice = createSlice({
    name: "date",
    initialState: {
        activeDate: dayjs(),
    },
    reducers: {
        setActiveDate: (state, action) => {
            state.activeDate = action.payload
        },
    },
})

export const { setActiveDate } = slice.actions
export default slice.reducer

在这个案例中我们可以看到,我们创建了一个切片,里面的initialState应该不难理解,就是仓库里基本的数据。但 reduceraction 分别是啥呢?

Reducer:仓库管理员

Reducer可以理解为仓库管理员,你要 存数据,改数据,扔数据 都得经过它的手,不能自己乱来~

Reducer有以下几点原则:

仅使用 state 和 action 参数计算新的状态值'

禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)

禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

虽然你是仓库管理员,但是你也不能乱来,你只能遵守规矩拿数据,因此咱们颁布一个 仓管行为准则action,仓管只能照规矩办事。

action:仓管行为准则

action可以理解为仓管行为准则,在代码中表现为一个具有 type 字段的普通 JavaScript 对象。

const getApple = {
    type: '苹果仓库/拿红色苹果'
}

const setApple = {
    type: '苹果仓库/放红色苹果',
    payload: '红色苹果'
}

Reducer仓库管理员 只能根据组件传入的 action仓管行为准则类型 基于当前的仓库执行指定操作

Reducer 是纯函数,基于先前的 state 和 action 来计算新的 state

useSelector():仓库查询器

通过仓库查询器可以查到执行仓库的的存货

React 组件使用 useSelector 钩子从 store 读取数据

选择器函数接收整个 state 对象,并且返回需要的部分数据

每当 Redux store 更新时,选择器将重新运行,如果它们返回的数据发生更改,则组件将重新渲染

简而言之用于获取指定的 模块数据,使用方法如下:

import { useSelector } from "react-redux"
//...
const dateState = useSelector((state) => state.apple)

其实就是将我们分片的模块给取出来的一个方法,很容易理解。

useDispatch():传递行为准则

useDispatch()是一个将 仓管行为准则action 发给 Reducer仓库管理员 的动作,如果组件想要对仓库执行某些操作就可以使用这个方法。

React 组件使用 useDispatch 钩子 dispatch action 来更新 store

在组件中调用 dispatch(someActionCreator()) 来 dispatch action

使用方法如下:

import { useDispatch } from "react-redux"
//...
const dispatch = useDispatch()
const setDate = (date) => dispatch(setActiveDate(date))

onClick={() => {
      setDate(item)
}}

Immutability:不可变性

不可变更新指的是,仓库管理员不可以直接变更仓库里的数据,只能把数据复制一份后再修改。举个例子,仓库管理员在操作仓库数据后会通过大喇叭广播告诉组件 “仓库里的数据有变化啦,快看看是不是和以前的数据不一样”。这是组件就会判断一下 仓库里新的数据与仓库里旧的数据是否相同 ,如果组件判断到数据更新了就会重新渲染或者执行一些操作。但是如果仓库管理员直接把数据给修改了,那么组件就没有办法判断数据的新旧了,导致数据变了但是组件却没有渲染新数据。

从代码层面看,因为js中的 数组和对象都是引用类型,当我们修改引用的数据时原本的数据也会发生更改,这样就导致我们没办法判断一个新数据与一个旧数据 是否不同,这一点在 react或者是vue的响应式 实现中都是容易踩坑的。

以下是官方文档中提到的不能在 Redux 中更改 state 几个原因:

  • 它会导致 bug,例如 UI 未正确更新以显示最新值
  • 更难理解状态更新的原因和方式
  • 编写测试变得更加困难
  • 它打破了正确使用“时间旅行调试”的能力

正确的修改方式应该是直接修改整个对象或者数组:

如果想要不可变的方式来更新,代码必需先复制原来的 object/array,然后更新它的复制体。

const obj = {
    a:1,
    b:2
}
//bad 
obj.b = 3

//good
obj = {...obj,b:3}

在使用 createSlice() 这个api创建切片时,我们传入的参数reducers属性中使用的方法可以直接修改state,因为createSlice 内部使用了一个名为 Immer 的库,帮助我们将更新转换为不可变更新,这样就方便很多啦~

单向数据流

单向数据流指的是,组件的权限只有 根据不同的仓库分类去拿资源 。组件必须遵守规则,不能自己去仓库里操作数据,一定要通过给仓库管理员派发一个行为准则,让仓库管理员帮你在指定的仓库里操作。

当然仓库里的资源如果变动了就会通知需要使用资源的组件 仓库内资源的新状态,这一步应该是通过 发布订阅 来实现的,组件使用仓库中的数据就会 订阅指定事件,组件使用的数据发生变化时就会通知所有订阅的组件用新的数据重新渲染。

这一步我看文档中有 subscribe(listener) 这个api,但在实际开发中还没有用上的场景,应该是在一些包中集成好了功能~

官方的这张图特别好,对于redux的数据流动有一定理解的时候来看会更加形象。

ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

当然也有 个人汉化版💩:

未命名文件.png

项目改造

具体的使用的话我还是根据我的TODO清单项目来改造,将原本写在组件中的数据逐步抽离到 redux 中。

首先安装以下相应的包:

yarn add redux react-redux @reduxjs/toolkit

目录结构

在根目录下创建store目录,并在 store 中创建 index.jsmodules 目录。目录都是个人习惯命名哈,随自己(公司)喜欢~modules 目录用来放我们的分片文件;index.js 是入口文件。

image.png

我们首先在 modules 中新建一个模块 date.js ,这个模块用来将之前日期选择栏中的 选中日期activeDate 给抽离出来方便复用

image.png

创建切片

在date.js中使用 createSlice 创建一个新切片并传入参数,createSlice不仅会帮我们创建切片,还可以提前为咱们创建好对应的 仓库管理员reducer仓管行为准则action

// store/modules/date.js
import { createSlice } from "@reduxjs/toolkit"
import dayjs from "../../utils/day"
export const slice = createSlice({
    name: "date",
    initialState: {
        activeDate: dayjs(),
    },
    reducers: {
        setActiveDate: (state, action) => {
            state.activeDate = action.payload
        },
    },
    
})

export const { setActiveDate } = slice.actions
export default slice.reducer

创建仓库

接下来我们创建一个仓库,并将切片放在仓库中,在store/index.js中写入代码如下:

// store/index.js
import { configureStore } from "@reduxjs/toolkit"
import date from "./modules/date"
export default configureStore({
    reducer: {
        date,
        // 这里放入各个模块
    },
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
            serializableCheck: false,
        }),
})

这里我还通过 middleware中间件 的配置项设置 关闭序列化检查,中间件是为了更方便的拓展redux的功能,这边我还没用到所以就只改了一下配置。之所以要关闭序列化检查是因为我的仓库数据是个 dayjs() 对象,不关闭会一直报错。

传入store

接下来我们在项目根目录的 index.js 中,使用 react-redux 中的 Provider 组件将根组件给包起来,然后把刚刚写的store传入

任何调用 useSelector 或 useDispatch 的 React 组件都可以访问 <Provider> 中的 store。

// index.js
import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
import { BrowserRouter } from "react-router-dom"

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

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </BrowserRouter>
  </Provider>,
  document.getElementById("root")
)
reportWebVitals()

修改代码

将原本使用 useState 创建的变量改为使用 useSelector 获取切片,然后使用 useDispatch 来创建 dispatch,并使用 dispatch 派发 action

//old
const [activeDate, setDate] = useState(dayjs())

//new 
import { useSelector, useDispatch } from "react-redux"
import { setActiveDate } from "../../redux/modules/date"

const activeDate = useSelector((state) => state.date.activeDate)
const dispatch = useDispatch()
const setDate = (date) => dispatch(setActiveDate(date))

这样 activeDate 这个变量我就可以在任意组件中引入了,这样即使组件继续增加,业务继续拓展我也不需要去修改原本的组件了~

这里不截图页面效果了,最终的页面效果是没有变化的,只是对于未来的开发效率很有帮助而已。

总结

对于一个有vuex的使用经验的开发者来说,redux还是有很大的不同的,主要是vuex帮我们做了太多的工作,导致对于这类 状态管理库 中的许多名词比较陌生,写完这篇文章对于我的提示蛮大的,开发思想提升了很多。

从零开始学react的系列已经一周啦,后面加快开发进度,在全部开发完成后再出一波大总结!

如果觉得这篇文章对你有帮助不妨点个赞,你的鼓励是我创作的最大动力~