近期,在学习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
- 官方文档:the
而至于useDispatch,我们依然先避开原生redux对dispatch的解释。在我看来,我们可以暂时理解为:通过redux toolkit我们已经定义了increment函数,只是在具体使用时我们需要再调用increment函数。而无论是redux、react-redux或是使用toolkit,由于state需要维持不可变性,我们都需要使用dispatch这个特殊方法来调用之前定义的函数,而不是像普通函数,直接increment()执行。
只是在react-redux中,dispatch被写为了一个钩子。
在官方文档中,也是推荐const dispatch = useDispatch()写法,因为useDispatch不接受参数
终于,我们完整地写出了一个redux组件。我们先来稍微总结一下
- 通过
configureStore创建一个用于保存state的store - 使用
createSlice创建一个切片,并设定好初始state和reducer用于修改state - 通过
Provider包裹整个App组件,使得在所有组件中都可以使用store中的state - 通过
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吗,那这两个钩子函数又是什么?其实,useSelector和useDispatch正是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"
})
}
}
}