简化 Redux 状态管理

事情是怎么变复杂的

Redux 本来并不复杂,其基本理念能够归纳为:经过 action 提交变动,通过 reducer 计算出新的 state。action 是一个约定含 type 字段的对象,reducer 是一个约定以 state 和 action 为参数、以新的 state 为返回值的纯函数。javascript

state -> dispatch(action) -> reducer -> new State => new View
复制代码

考虑最原始的情形,在业务代码中直接 dispatch action 对象,那么只须要定义三个文件就能够了:state.js、reducer.js、store.js,分别用于定义 state、reducer 和建立 store。html


一、不要直接提交 action 对象

业务代码直接 dispatch action 对象并很差,一是重复,二是无类型校验。官方推荐用函数来建立 action,而且 action 的 type 最好用常量而不是字符串,同时还需保证 type 的全局惟一性。因而再加两个文件:actions.js、type.js,分别用于定义 action 函数和 type 常量。vue

这就造成了广泛遭受诟病的模板代码(常量还得全大写,害),因而出现了一些方案,来简化 action 与 type 的定义,好比 reduxsause、react-arc。java


二、reducer 须要拆开

官方仅仅约定了 reducer 的 interface,但没有规定具体实现。原始案例中用 switch,每一个 case 对应一个 action,当 action 增多时,一个超长的 reducer 显然是不利于维护的。官方提供了一种拆分思路,并提供了相应的辅助函数(combineReducers)。react

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}
复制代码

不过这种模式有不少问题,好比:每一个字段都须要定义一个子 reducer;子 reducer 的 state 参数都不同,而且不能忘了设置初始值;同一个 action 可能分布在不一样的子 reducer 中,每新增一个 action,若是对应多个字段的变动,那么须要在多个 reducer 中新增 case 分支。git


三、深层数据结构如何更新

redux 约定 state 必须是全局惟一而且是 immutable 的(约定,而非约束),reducer 每次都须要整个的返回一个新的 state。若是数据结构比较深,更新起来很麻烦。这个问题相对好解决,一般用 immutable-helper、seamless-immutable 之类的辅助库便可。github


四、异步更新逻辑怎么办

好像全部介绍 Redux 的文章都会不约而同的宣称一个极具误导性的论断:Redux 不支持异步状态更新。vuex

那是否是说,若是单纯用 Redux,应用里的异步逻辑就没法写了?显然没有这回事。typescript

// 照常写异步逻辑,而后提交更新,有啥问题咩?
fetch().then(data => {
    dispatch(updateAction(data))
})
复制代码

不过有人会这么想:我就想提交一个 action,由这个 action 去完成异步逻辑,人家 vuex 和 mobX 都有相关的支持,redux 咋就不行?redux

因而官方经过 redux-thunk 提供了一个语法糖,让你能够 dispatch 一个封装了异步逻辑的 thunk 函数,使得代码在语义上可以实现“提交了一个异步 action”这么个事儿。

// 寥寥数行的 redux-thunk 中间件
function createThunkMiddleware() {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') { // 啊哈
      return action(dispatch, getState)
    }
    return next(action)
  };
}
复制代码

看清本质你就会明白,哪里有“redux 不支持异步状态更新”这回事儿呢?只是代码逻辑聚合在哪里的区别而已,最终要触发 reducer 更新 state,仍是得在 dispatch 一个对象的时候。因此根本不存在“非得引一个库 redux 才能支持异步”这回事儿,不管是 redux-thunk、redux-promise 仍是 redux-saga,与其说在弥补 redux 的缺陷,倒不如说它们在解决本身额外创造出来的问题。

偏激的讲,【async action】 是一个与 redux 无关的概念陷阱。客观的说,【async action】是一种代码设计抽象。原本 action 就是一个纯对象而已,如今 action 还能够是一个函数,被称为异步 action。

const a = { type, payload } // 这是一个 reducer action
const b = payload => ({ type, payload }) // 这也是一个 reducer action
const c = dispatch => fetch().then(res => dispatch(xxx) // 这是一个 async action
复制代码

任何状态管理库,提供的所谓异步状态更新功能,都只是一种 api 层面上的包装。这固然有好处,好比提供了额外的抽象约束,状态更新逻辑更加内聚,同时让业务逻辑更纯粹,组件只管发 action 和呈现数据。但不能认为“redux 不支持异步 action,因此必须加 thunk,必须加 saga”,这是站在抽象的高峰,被概念的云雾迷了眼。


Why do we need middleware for async flow in Redux?


小结

本来只是三个文件的事,最终扩展成了 redux + react-redux + action type 定义方案 + immutable 数据更新方案 + 异步过程封装方案。这下好了,要写一个最简单的“请求接口而后更新数据”的逻辑,每每须要改动 五、6 个文件。整个逻辑链路之长,不只写起来费劲,看/找起来也费劲。

因而社区给出了整合、封装甚至重构过的相对完整的类 redux 方案,好比 rematch(Redesigning Redux)、dva。更激进的,有人不肯意受“只能经过 dispatch action 而不是直接调用 reducer 更新 state”的约束,搞出了【action reducer 化】的方案,好比 redux-zero、reduxless。

// redux
dispatch(action) => reducer => newState

// redux-zero
action(state, payload) => newState
复制代码

围绕 redux,不只有大量“修补型”方案,还有很多“整合型”、“重构型”和“替代型”方案,正面的说,这是生态繁荣的体现,负面的说,这是对 redux 做为事实上的【react 状态管理业界标准】的某种讽刺。这一切究竟是证实 redux 自己不是一个好的设计,仍是说使用者没用对,把事情搞复杂了?


一种自动生成 action 的简化方案

每个 action,必定对应一个 reducer 处理逻辑,若是 reducer 函数按 action 粒度拆分,每一个 action,对应一个 reducer 函数,而每一个 action 拥有惟一的 type,那么能够得出:action、type 和 reducer 是一一对应的。

若是能保证 reducer 不重名,而后 action 和 type 直接复用 reducer 的名称,那么 action 就能根据 reducer 自动生成。

// reducer.js
// reducer 的拆分方式有不少
// 这里每一个 reducer 对应一个 action
// 第一个参数是全局的 state,第二个参数对应 action 中的 payload
export const reducerA = (state, payload) => state
export const reducerB = (state, payload) => state

// action.js
// 指望根据 reducer 自动生成的 actions 对象
export const actions = {
    reducerA: payload => ({ type: 'reducerA', payload }),
    reducerB: payload => ({ type: 'reducerB', payload })
}
复制代码

首先,按上述方式拆分的 reducer,须要按以下方式聚合:

// store.js
import initialState from './state.js'
import * as reducers from './reducer.js'

// 聚合 reducer
function reducer(state = initialState, { type, payload }) {
  const fn = reducers[type];
  return fn ? fn(state, payload) : state;
}
复制代码

上述写法要求全部的 reducer 都聚合在 reducer.js 里,注意,不必定都定义在 reducer.js 里,能够分散定义在不一样文件中,只是在 reducer.js 里统一导出,这样就保证了 reducer 不会重名。


有了 reducer 的 map 对象,很容易自动生成 actions 对象:

// action.js
import * as reducers from './reducer.js'

export const actions = Object.keys(reducers).reduce(
  (prev, type) => {
    prev[type] = payload => ({ type, payload })
    return prev
  },
  {}
)
复制代码

使用的时候,引入 actions 对象便可:

import { actions } from 'store/action.js'

dispatch(actions.reducerA(payload))
复制代码

这样,action 和 type 就都不须要定义了。每次新增逻辑,状态部分就只须要写 state 和 reducer 便可。

不过上述方式仍然不够完美,由于没有类型。按常规的写法,action 是有类型定义的,既能够校验参数,又有自动补全提示,可丢不得。

// reducer.ts
export interface A {}
export const reducerA = (state: StateType, payload: A) => state

// action.ts
// 常规写法
import { A } from './reducer.ts'
export const actionA = (payload: A) => ({ type: TYPE, payload })
复制代码

考虑到 action 函数的参数类型和对应 reducer 第二个参数的类型是一致的,那么可否既复用 reducer 的名称,又复用参数类型,在自动生成 actions 对象的同时,连类型也一块儿自动生成呢?

// 指望生成的 interface
// 关键是如何拿到 reducer 函数定义好的 payload 参数的类型,返回值类型其实不须要关心
interface Actions {
    reducerA (payload: A): AnyAction
    reducerB (payload: B): AnyAction
}
复制代码

首先不考虑 payload 参数类型,先看如何自动生成 actions 对象的 interface:

可见,经过 keyof 关键字,Actions 类型已经拿到了全部的键值。

接下来要设置 payload 参数类型,关键是如何拿到一个已知的函数类型定义中的第二个参数的类型。TS 里提供了 infer 关键字用于提取类型。

T 表示 args 的类型,是一个数组,T[1] 即第二个参数的类型。

搞定了以上两个关键步骤,剩下的事情就比较简单了:

这样咱们就有了类型,每次新增 reducer,都会自动生成最新的 actions 及其类型。

省略了 action 定义,也就省掉了模板代码中的一大半。其它减省代码的地方还有:mapDispatchToProps 使用官方推荐的简写形式、用 class 定义 state 以直接提取 StateType 类型、封装 store 定义等,细枝末节很少赘述。

完整案例可参见:codesandbox.io/s/clever-ra…

相关文章
相关标签/搜索