让所有人都能学懂的redux入门

2,529 阅读11分钟

近期,在学习redux的时候,查阅了许多文档,发现由于redux官方目前已经推荐redux toolkit,而网上许多文档依然是从下到上介绍原生redux,在实际使用时许多方法已经不被官方推荐了,所以就浅浅整理了一篇,思路与官方文档的入门类似,但对详略有所改变,并加了一些自己的想法,可能会对初学者入门更加友好。

跳过新手村,直接使用redux toolkit

如果你已经有接触过一部分的redux,知道action、dispatch、subscribe等等,我建议我们可以先“忘记”他们,而回归到最本质的问题——redux要做什么

新建项目

react项目结构如下

 |--src
   |--components #UI组件
   |--containers #逻辑组件
   |--store #redux
     |--features #redux组件
   |--type #ts类型定义

redux的目标

由于react的单向数据流问题,导致state状态传递和复用十分困难。比如一个组件向兄弟组件传递信息时,需要先传入父组件,再传到兄弟组件,十分的不方便。或者在不太相关的一个A组件中,使用B组件的状态,都是难以实现的。

于是,redux想出一个办法:将所有需要复用的状态集中存放在一起,就可以在任意组件中调用需要的状态。 而存放这些state的一个集中的库,我们就叫它store

创建store

redux toolkit提供了configureStore方法来创建store。

 //在store目录下创建store.tsx
 import {configureStore} from '@reduxjs/toolkit'; // 引入configureStore方法
 ​
 const store = configureStore({})

现在,我们已经有地方来存放state了,但我们应该怎样存放、或取出使用呢?

创建切片

在原生react中,如果我们想要改变state,就需要用setstate,而不是直接state=0这样的方式,因为React期望所有状态更新都是使用不可变(immutable)的方式。简单来说,当我们想要改变state时,需要用一个专门的方法去改变,而不是直接赋值的方法改变。而redux也遵循了这样的原则,但redux为setstate另取了一个名字,叫reducer。

OK,到这里我们已经可以使用redux toolkit来写一个redux组件了,后续需要用到的其他方法在使用时再解释,我们要记住:redux的最初目的是为了为state提供集中的状态管理,其他所有方法都只是附加


redux toolkit提供了一个十分便利的方法createSlice,来以对象的形式编写一个redux切片(我并不想叫它redux实例或者redux组件,因为很容易和后面的真实组件混淆,所有直接选择了直译为“切片”,官方文档也是称为切片。事先声明,本人不看vtuber,更不知道什么嘉然今天吃什么[doge]

关于为什么redux要名命名为 reducer和 createSlice,可以从 array.reducer和 array.slice理解。

 //在features中新建counterSlice.tsx
 import { createSlice } from '@reduxjs/toolkit' // 引入createSlice方法
 ​
 //创建counterSlice实例并导出
 export const counterSlice = createSlice({
     name: 'counter',
     //在创建实例时给定state
     initialState:{
         value:0
     },
     reducers: {
         increment: (state) => {
             /**
              * Redux Toolkit 允许我们在还原器中编写“可变的(mutable)”逻辑。
              * 它实际上并没有改变状态,因为它使用 Immer 库,
              * 它将检测对"draft state" 的更改,并根据这些更改生成一个全新的不可变状态
              */
             state.value += 1
         },
         decrement: (state) => {
             state.value -= 1
         },
     },
 })
 ​
 // 为每个 reducer 函数生成动作创建器(Action creators),action是什么之后会讲到
 //这里export的方法是为了给counter组件,而不是给redux或store
 export const { increment, decrement } = counterSlice.actions
 ​
 //export给store
 export default counterSlice.reducer

其实仔细看createSlice方法,会发现它很像react的useState钩子,一个initialState,一个改变state的方法,其中的name是无关紧要的,而通过redux toolkit编写切片,就像创建了多个state钩子,只是其中的state不是保存在特定组件,而是集中存放在store中。

此时,我们再在store.tsx中引入counterSlice的reducer

 import {configureStore} from '@reduxjs/toolkit';
 ​
 //一般都以组件名xxxReducer方式命名,而不叫xxxSlice,因为这里import的是导出的counterSlice.reducer
 import counterReducer from "./features/counterSlice";
 ​
 ​
 // configureStore创建一个redux数据
 const store = configureStore({
     reducer: {
         counter: counterReducer,//这里给定的名字最好与counterSlice中的name相同
     },
 });
 ​
 export default store

至此,我们已经成功创建了store,并在store中保存了counterSlice这个“切片”。接下来就是如何将counterSlice放入页面中

将store传入组件

如果我们想要调用store中的state,我们就需要将store传入我们需要的组件中,react-redux为我们提供了一个这样的方法:使用Provider组件进行包裹

 //修改index.tsx
 import React from 'react';
 import ReactDOM from 'react-dom/client';
 import './index.css';
 ​
 import {Provider} from "react-redux";
 ​
 import App from './App';
 ​
 import store from "./store/store"; // 引入创建的store
 ​
 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
 );
 root.render(
   <React.StrictMode>
       <Provider store={ store }>
           <App />
       </Provider>
   </React.StrictMode>
 );

通过使用Provider包裹App,我们可以在App中任意一个组件中调用store的任意切片

Provider原理是使用react提供的Context属性,所以为什么Provider是react-redux中的内容,而不在redux中

 class Provider extends Component {
   getChildContext() {
     return {
       store: this.props.store
     };
   }
   render() {
     return this.props.children;
   }
 }
 ​
 Provider.childContextTypes = {
   store: React.PropTypes.object
 }

编写组件

这部分就是我们所熟悉的组件的编写,但我们仍需要使用一些redux中的方法来控制store

 //在containers中创建counter.tsx
 //这里我个人认为算是逻辑组件,所以并没有写到components里,不知道我的理解有没有问题
 import React from 'react'
 ​
 //两个都是react-redux的钩子函数
 import { useSelector, useDispatch } from 'react-redux'
 ​
 //之前counterSlice导出的方法就直接用在组件上
 import { decrement, increment } from '../store/features/counterSlice'
 ​
 export default function Counter() {
     const count = useSelector((store:any) => store.counter.value)
     const dispatch = useDispatch()
     return (
         <div>
             <div>
                 <button onClick={() => dispatch(increment())}>
                     增加+
                 </button>
                 <span>{count}</span>
                 <button onClick={() => dispatch(decrement())}>
                     减少-
                 </button>
             </div>
         </div>
     )
 }
  • useSelector:

    • 官方文档:the useSelectorhook lets your React components read data from the Redux store
    • userSelector读取store中的数据并放入组件中
    • useSelector接受一个函数作为参数,这个函数的参数是整个store
    • 这里由于store.tsx中命名为counter,故是store.counter,而counterSlice中initialState有value,所以最终我们需要的数据是store.counter.value

而至于useDispatch,我们依然先避开原生redux对dispatch的解释。在我看来,我们可以暂时理解为:通过redux toolkit我们已经定义了increment函数,只是在具体使用时我们需要再调用increment函数。而无论是redux、react-redux或是使用toolkit,由于state需要维持不可变性,我们都需要使用dispatch这个特殊方法来调用之前定义的函数,而不是像普通函数,直接increment()执行。

只是在react-redux中,dispatch被写为了一个钩子。

在官方文档中,也是推荐const dispatch = useDispatch()写法,因为useDispatch不接受参数


终于,我们完整地写出了一个redux组件。我们先来稍微总结一下

  1. 通过configureStore创建一个用于保存state的store
  2. 使用createSlice创建一个切片,并设定好初始state和reducer用于修改state
  3. 通过Provider包裹整个App组件,使得在所有组件中都可以使用store中的state
  4. 通过useSelector选出需要的state并用useDispatch执行需要改变的内容

如果你只是想学会怎样应用redux,其实学到这里也已经足够了,可以看出,通过redux toolkit上手redux十分简单,只是相当于写了一个稍微复杂一点的useState钩子。

接下来要说的,就是redux自身的设计。

原生redux

三大核心概念

redux三大核心概念:store、reducer、action,store由于本身就较简单,之前介绍过,这里就不再介绍。

先说说之前没有介绍的action,其实,在toolkit中,我们已经用到了action,只是当时没有详细讲解

 //counterSlice.tsx中
 export const { increment, decrement } = counterSlice.actions

在原生redux中,action描述应用程序中发生了什么的事件。action是一个对象,且这个对象中必须有一个type属性

 //一个典型的action对象
 const addTodoAction = {
   type: 'todos/todoAdded', // 必需的属性
   payload: 'Buy milk' // 附加的属性
 }

故上面的increment和decrement也只是一个对事件的描述,而不是真实的函数,所以这也解释了为什么需要通过dispatch去触发这个事件描述

而reducer虽然概念和之前的相似,但在具体实现上,redux最先并没有采取“切片”的方式,更没有钩子这种模型写法,而是像一个普通的函数。reducer接收当前state和一个action对象作为参数,并返回处理后的新state。你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。虽然和toolkit的调用方法相差很大,但reducer都是为了去处理action事件。

 const initialState = { value: 0 }
 ​
 function counterReducer(state = initialState, action) {
   // 检查 reducer 是否关联这个 action
   if (action.type === 'counter/increment') {
     // 如果是,复制 `state`
     return {
       ...state,
       // 使用新值更新 state 副本
       value: state.value + 1
     }
   }
   // 返回原来的 state 不变
   return state
 }

而如果在redux中想要使用多个reducer,就要使用combineReducers,相比于用切片的形式,麻烦了许多

你可能会突然发现,好像reducer和dispatch功能十分相像,都是为了去根据action,来修改state(当然,这是我讲解思路的问题,为了在开始有助于理解而扭曲了dispatch)。所以,我们通过讲解数据流,来认识到reducer和dispatch的不同点

数据流

react的单向数据流如下

  • 用 state 来描述应用程序在特定时间点的状况
  • 基于 state 来渲染出 View
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新,生成新的 state
  • 基于新的 state 重新渲染 View

而由于redux是集中状态管理,所以action到state这步如果采用可变的state,就十分容易出错。

于是redux设计了这样的模式:action -> dispatch -> reducer->(subscribe->getState) => new state。其中action是负责描述状态变化的,dispatch负责分发action,reducer就负责根据dispatch分发的action,修改状态。

所以之前所说的dispatch用于执行函数是不太准确的,但考虑到初学者的理解 (因为本人初学时对这些概念比较吃力),我还是决定在前面保留这个“错误”说法

故redux的数据流如下

  • 初始启动:

    • 使用最顶层的 root reducer 函数创建 Redux store
    • store 调用一次 root reducer,并将返回值保存为它的初始 state
    • 当 UI 首次渲染时,UI 组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
  • 更新环节:

    • 应用程序中发生了某些事情,例如用户单击按钮
    • dispatch一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'})
    • dispatch后,store会调用reducer函数,参数即为之前的 state 和当前的 action ,并将返回值保存为新的 state(这一步是真正改变state的步骤)
    • store 通知所有订阅过的 UI,通知它们 store 发生更新
    • 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
    • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

所以,dispatch只是告诉store有什么action,然后store根据dispatch分发的action去调用对应的reducer,从而改变state

(但我个人也会有点疑惑,为什么在 redux toolkit中,不将 dispatch和 reducer合并成一个语法糖,我们在使用 increment函数时就可以直接调用,而不用再 dispatch,也可能 redux故意这样设计,让我们不能轻易地去改变 store库)

由于现在redux官网的数据流讲解未使用到subscribe和getState,并且本人目前也还未完全理解,就先不讲解了

react-redux

其实如果阅读官方文档,会发现基本引用的库都是react-redux+redux toolkit,已经基本不会使用原生redux了,而网上依然有很多人混乱地去引用不同的库。所以在这部分,就梳理一下到底哪部分才是react-redux所带的

其实,真正属于react-redux的只有两个:Provider和connect,Provider我们肯定有印象了,用于将store传入组件中,但这个connect好像之前完全没讲呢。让我们再回去仔细看代码

 //counter.tsx
 ​
 import { useSelector, useDispatch } from 'react-redux'

不是说react-redux只有Provider和connect吗,那这两个钩子函数又是什么?其实,useSelectoruseDispatch正是connect的“语法糖”。

现在在官方文档已经找不到connect了,但网上一些文章仍然保留了,所以关于connect的说明可能会不准确,但这部分完全可以不用深入了解,甚至不了解也不影响对redux的编写,因为 useSelector和 useDispatch已经做的很完美了

但在没有这两个如此方便的钩子函数时,想要调用store里的state是麻烦许多的,必须要用connect方法去关联组件和store

 import { connect } from 'react-redux'
 ​
 // connect执行返回一个高阶组件
 // 在connect后,Counter组件才有dispatch等方法
 export default connect( mapStateToProps , mapDispatchToProps )(Counter)
 // connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 UI 组件的业务逻辑。
 // mapStateToProps负责输入逻辑,即将state映射到 UI 组件的参数(props)
 // mapStateToProps负责输出逻辑,即将用户对 UI 组件的操作映射成 Action
 ​
 //映射state到组建的props上
 function mapStateToProps(state){
     return {
         value:state.counter
     }
 }
 ​
 //映射dispatch方法到组建的props上
 function mapDispatchToProps(dispatch){
     return {
         increment(){
             dispatch({
                 type:"increment"
             })
         },
         decrement(){
             dispatch({
                 type:"decrement"
             })
         }
     }
 }