本文完整例子链接 ,github 链接。
redux 和 React 没有半毛钱关系。redux 是一个独立的数据管理方案。
action 是对数据来源的封装,它被用来描述数据变化的信息,提供一组用来描述行为的数据。action 不是数据本身,也不是修改数据的行为,是行为的描述。
在 JS 里面描述一种行为很简单,因为行为实质是函数,只要辨别是使用了哪个函数,参数是什么,就行了。所以这也常常就是 Action 的数据结构:
// 伪代码
action = {
type: 指定某个 action ,
[... args : 执行行为所需的参数]
}
官方定义如下:
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
从 js 编程的方面来理解,action 是一个个对象,从 ts 的类型上来说,他们的组合就是辨识联合。
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
一一官文中译
reducer 接收数据 State 和一个 action ,根据 Action 去改变数据 State 并返回。
(previousState, action) => newState
上述的函数就是 reducer。名字的由来是 Array.protorype.reduce ,都是接收旧状态返回新状态。reducer 被规定不能有以下操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如 Date.now() 或 Math.random()。
只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
Action 描述行为;
Reducer 根据 Action 来更新 State;
那么 Store 就是把 Action 和 Reducer 联系起来的对象。Store 维持 State 状态,并且提供获取 State,分发 Action 到 Reducer,监听 State 变化的等功能。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
尝试使用 redux:
import {
createStore } from "redux";
// 数据类型
type State = Array<{
data: string }>;
// Action 辨识
enum DISC {
ADD = "LIST_ADD",
UPDATE = "LIST_UPDATE",
}
interface Action_Add {
type: DISC.ADD;
data: string;
}
interface Action_Update {
type: DISC.UPDATE;
data: string;
index: number;
}
type Actions = Action_Add | Action_Update;
// 添加数据的描述
export function listAdd(data: string): Action_Add {
return {
type: DISC.ADD, data };
}
// 修改数据的描述
export function listUpdate(data: string, index: number): Action_Update {
return {
type: DISC.UPDATE, data, index };
}
// 实例数据的 reducer
function list(state: State = [], action: Actions): State {
switch (action.type) {
case DISC.ADD:
return [...state, {
data: action.data }];
case DISC.UPDATE:
return state.map((val, index) => {
return index === action.index ? {
data: action.data } : val;
});
default:
return state;
}
}
// 创建一个 Store 实例
const store = createStore(list, [{
data: "111" }]);
// 监听数据
const unLister = store.subscribe(() => {
console.log(...store.getState());
});
store.dispatch(listAdd("something"));
store.dispatch(listAdd("something tow"));
store.dispatch(listAdd("something three"));
store.dispatch(listUpdate("aaaa", 1));
store.dispatch(listUpdate("bbbb", 2));
store.dispatch(listUpdate("cccc", 3));
store.dispatch(listUpdate("dddd", 4));
// 消除数据监听
unLister();
写这段文章的时候我对国内的社区十分灰心,没有一个人或者一篇文章,好好介绍了异步 Action 结合typescript 的写法,因为官方文档和中译对此几乎都没介绍,国内社区全部是复制粘贴。
最后能正确使用 ts ,是在redux-chunk 源码看到 ts 示例。
异步 Action 和同步的完全不一样,异步 Action 没有提供行为的基本信息,也没有提供行为的辨识。它不由 Reducer 来和数据做联系,而是被 thunk-redux 中间件直接处理。
异步 Action 一般长下面这个样子:
// es5
function asyncAction () {
return function handle (dispatch , getState , extraArgument ) {
return result = 'any result';
}
}
// es6
asyncAction = () => (dispatch,getState,extraArgument) => result = 'any result';
异步 Action 并不是返回数据,甚至大多数情况下,异步 Action 都没有返回值或返回一个状态。
应用场景: 所有需要异步控制 State 的场景,都适合使用异步 Action。虽然不像普通 Action 一样提供行为描述的信息给 Reducer 操作,但是异步 Action 也是对行为的描述,不过是异步行为。
请求一组异步数据:
import {
createStore, applyMiddleware } from "redux";
import thunk, {
ThunkAction, ThunkMiddleware } from "redux-thunk";
import api from "../api";
// 数据类型
type State = Array<{
data: string }>;
// Action 辨识
enum DISC {
INIT = "LIST_INIT",
}
interface ActionInit {
type: DISC.INIT;
datas: State;
}
type Actions = ActionInit;
type ThunkReturn<R> = ThunkAction<R, State, unknown, Actions>;
// 修改数据的描述
export function listInit(datas: State): ActionInit {
return {
type: DISC.INIT, datas };
}
// 异步 Actions
export const asyncGetData = (): ThunkReturn<void> => dispatch => {
api.getData().then((datas: State) => dispatch(listInit(datas)));
};
// 实例数据的 reducer
function list(state: State = [], action: Actions): State {
switch (action.type) {
case DISC.INIT:
return [...action.datas];
default:
return state;
}
}
// 创建一个 Store 实例
const store = createStore(
list,
applyMiddleware(thunk as ThunkMiddleware<State, Actions>)
);
store.subscribe(() => console.log(...store.getState()));
// 分发异步actions
store.dispatch(asyncGetData());
标记下异步 Action 常用的类型:
// redux-think
// S:数据类型,E:额外参数,A:Action 类型
export interface ThunkDispatch<S, E, A extends Action> {
<T extends A>(action: T): T;
<R>(asyncAction: ThunkAction<R, S, E, A>): R;
}
// R:返回值类型,S:数据类型,E:额外参数类型,A:Action 类型
export type ThunkAction<R, S, E, A extends Action> = (
dispatch: ThunkDispatch<S, E, A>,
getState: () => S,
extraArgument: E
) => R;
// S:数据类型,A:Action 类型,E:额外参数类型
export type ThunkMiddleware<S = {
}, A extends Action = AnyAction, E = undefined> = Middleware<ThunkDispatch<S, E, A>, S, ThunkDispatch<S, E, A>>;
同各大框架的中间件类似,redux 的中间件解决的是在分发行为后,行为执行前做的事。并且支持链式注册。
官方文档有 Middleware 的演变过程,一个 Middleware 定义如下:
/**
* @template DispatchExt 为 Dispatch 提供额外的扩展签名.
* @template S 中间件支持的 state 类型.
* @template D 分发类型。
*/
interface Middleware<
DispatchExt = {
},
S = any,
D extends Dispatch = Dispatch
> {
(api: MiddlewareAPI<D, S>): (
next: Dispatch<AnyAction>
) => (action: any) => any
}
定义一个日记记录的的中间件并应用:
import {
createStore, applyMiddleware, Middleware } from "redux";
// 数据类型
type State = Array<{
data: string }>;
// Action 辨识
enum DISC {
ADD = "LIST_ADD",
}
interface ActionAdd {
type: DISC.ADD;
data: string;
}
type Actions = ActionAdd;
// 修改数据的描述
export function listAdd(data: string): ActionAdd {
return {
type: DISC.ADD, data };
}
// 实例数据的 reducer
function list(state: State = [], action: Actions): State {
switch (action.type) {
case DISC.ADD:
return [...state, {
data: action.data }];
default:
return state;
}
}
// 中间件
const logger: Middleware<{
}, State> = store => next => action => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};
// 创建一个 Store 实例
const store = createStore(list, applyMiddleware(logger));
store.dispatch(listAdd("content"));
react-redux 就是把 redux 放到了 react 中。具体的实现方式就是写了一个超级 Context,然后在应用的根部放入 Provider
组件,然后通过一个叫 connect
的高阶函数注入到需要使用 redux 数据的组件内。
import React from "react";
import TestComponent from "./components/TestComponent";
import {
Provider } from "react-redux";
import store from "./store";
function App() {
return (
<Provider store={
store}>
<div className="App">
<TestComponent />
</div>
</Provider>
);
}
export default App;
就是那么简单就行了,不过事先要安装 react-redux:npm i -S react-redux
。
其中 store 就是上面一节中已经注册好了的。
connect 被用来连接到 react-redux 的 Provider
。connect 是一个高阶函数,返回一个高阶组件。
connect 可以通过传入需要一个回调设置到需要的状态,通过传入一堆 action 来设置被封装后的 action。这些设置会被封装到 connect 返回的高阶组件中,然后透过透传的方式,被升阶的组件可以直接从 props
里拿到这些状态和被封装的 action。
如果不想封装 action,可以不传入 action,那么可以在返回的高阶组件中 props
中拿到一个 props.diapatch
属性,用来自己分发 action。
connect一般用法如下:
// mapStateToProps 设置需要的状态,mapDispatchToProps 设置需要的分发的 action
connect(mapStateToProps,mapDispatchToProps)(Components);
// 有可能你不需要获取状态,那么第一个值可以传 null
connect(null,mapDispatchToProps)(Components);
// 也可能都不需要,自己通过 `props.dispatch` 分发
connect()(Components);
mapStateToProps 类型:
/**
* 创建需要获取的状态
*/
const mapStateToProps = (state: RootState) => {
return {
student: state.student,
};
};
这个简单,直接设置就行。
mapDispatchToProps 类型:
/**
* 创建需要调用的 dispatch,绑定 action 创建函数。
* 按照 props.action() 调用,但实际上执行了 dispatch(action)
* @param dispatch 用以分发的 dispatch
*/
const mapDispatchToProps = (dispatch: MapDispatch) => ({
/**
* 普通 action 绑定可以通过快捷方式 bindActionCreators 创建,这段代码等价于
* ```js
* ...{
* addStudent:(name:string,grade:Grade) => dispatch(addStudent(name,grade)),
* updateStudent: (index: number, name: string, grade: Grade) => dispatch(index,name,grade),
* ......略
* }
* ```
*/
...bindActionCreators({
addStudent, updateStudent, deleteStudent }, dispatch),
/**
* 虽然异步 action 也可以使用 bingActionCreators 创建,但是返回值类型会出问题,
* 所以还是需要单独写,react-redux 和 thunk-redux 并不是完全兼容,
*/
asyncGetStudent: () => dispatch(asyncGetStudent()),
});
有时不使用 mapDispatchToProps ,直接用 props.dispatch
来转发,那么在组件的 props
类型里还要加入一个 diapatch 类型。详见实例代码。
可以清楚的看到,在 react-redux 中,connect
是一个高阶函数,返回值是一个高阶组件。高阶组件有好有坏,这种问题使用 Hook 来解决会更方便些。
react-redux 迭代几个版本后,剩下 3 个核心的 hook:
这三个 hook 使得使用 react-redux 更加简单:
useSelector 提供对 redux 中状态的获取。
语法:
const result: any = useSelector(selector: Function, equalityFn?: Function)
useSelector 类似于 mapStateToProps
,他们的参数存在一定的差异,这些差异来自 hook 的特性:
prop
。在 mapState
中,因为状态存到了 connect
高阶函数返回的高阶组件中,所有状态被合并到对象中返回,返回的对象是否是新引用并没有多少关系,所以 connect
只是比较各个值。
使用 useSelector()
,因为 hook 的特性,每次默认返回一个新对象的话就会强制重新渲染。可以把对象拆分,多次使用 useSelector
减少渲染:
useSelector
shallowEqual
来作为 useSelector
的第二个参数:import {
shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
memoizing selector 能有效减少计算的次数,memoizing selector 需要借助于库 reselect。这个东西类似于 Vue 里面的计算属性,专门针对计算的优化:
react-redux 官网示例(不是 TSX):
import React from 'react'
import {
useSelector } from 'react-redux'
import {
createSelector } from 'reselect'
/**
* createSelector(... inputSelectors | [inputSelectors],resultFunc)
* 至少2个参数
* inputSelectors 是函数,其参数有 createSelector 的返回值提供。作用是对状态的获取,返回值将依次传递给 resultFunc
* resulFunc 对状态进行计算并返回值
* 返回值:返回一个处理后的 selector,参数将传递给 inputSelectors,结果由 resultFunc 提供
*/
const selectNumOfTodosWithIsDoneValue = createSelector(
state => state.todos,
(_, isDone) => isDone,
(todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)
export const TodoCounterForIsDoneValue = ({
isDone }) => {
const NumOfTodosWithIsDoneValue = useSelector(state =>
selectNumOfTodosWithIsDoneValue(state, isDone)
)
return <div>{
NumOfTodosWithIsDoneValue}</div>
}
export const App = () => {
return (
<>
<span>Number of done todos:</span>
<TodoCounterForIsDoneValue isDone={
true} />
</>
)
}
useDispatch 就是单纯的获取到分发器,从而分发各个行为。没有参数,直接调用即可。
useStore() 获取状态机。没有参数,直接调用。
参考链接: