跳到主要内容

Redux 高阶运用

高阶reducer

在 Redux 架构中,reducer 是一个纯函数,它的职责是根据 previousState 和 action 计算出新的 state。在复杂应用中,Redux 提供的 combineReducers 让我们可以把顶层的 reducer 拆分成多个小的 reducer,分别独立地操作 state 树的不同部分。而在一个应用中,很多小粒度的 reducer 往往有很多重复的逻辑,那么对于这些 reducer,如何去抽取公用逻辑,减少代码冗余呢?这种情况下,使用高阶 reducer 是一种较好的解决方案。

在讲述如何使用高阶 reducer 抽取公用逻辑之前,我们先来定义高阶 reducer 的概念。我们之前对函数式编程已经有所了解,知道高阶函数是指将函数作为参数或者返回值的函数。

类似地,高阶 reducer 就是指将 reducer 作为参数或者返回值的函数。

有没有意识到 combineReducers 其实就是一个高阶 reducer。因为 combineReducers 就是将一个 reducer 对象作为参数,最后返回顶层的 reducer。 下面我们将以两个典型的案例给大家讲述高阶 reducer 的常见使用方法。

  1. reducer 的复用

我们将顶层的 reducer 拆分成多个小的 reducer,肯定会碰到 reducer 的复用问题。例如有 A 和 B 两个模块,它们的 UI 部分相似,此时可以通过配置不同的 props 来区别它们。那么这种情况 下,A 和 B 模块能不能共用一个 reducer 呢?答案是否定的。我们先来看一个简单的 reducer:

const LOAD_DATA = 'LOAD_DATA'; 
const initialState = { /* ... */ };

function loadData() {
return {
type: LOAD_DATA,
...
};
};
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_DATA:
return {
...state,
data: action.payload,
};
default:
return state;
}
}

如果我们将这个 reducer 绑定到 A 和 B 这两个不同的模块,造成的问题将会是,当 A 模块调用 loadData 来分发相应的 action 时,A 和 B 的 reducer 都会处理这个 action,然后 A 和 B 的内容就完全一致了。

这里我们需要意识到,在一个应用中,不同模块间的 actionType 必须是全局唯一的。

因此,要解决 actionType 唯一的问题,有一个方法就是通过添加前缀的方式来做到:

function generateReducer(prefix, state) {
const LOAD_DATA = prefix + 'LOAD_DATA';
const initialState = { ...state, ... };
return function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_DATA:
return {
...state,
data: action.payload
};
default:
return state;
}
};
}

这样只要 A 模块和 B 模块分别调用 generateReducer 来生成相应的 reducer ,就能解决 reducer 复用的问题了。而对于 prefix,我们可以根据自己的项目结构来决定,例如 ${页面名 称}_${模块名称}。只要能够保证全局唯一性,就可以写成一种前缀。

  1. reducer 的增强

除了解决复用的问题,高阶 reducer 的另一个重要作用就是对原始的 reducer 进行增强。 redux-undo 就是典型的利用高阶 reducer 来增强 reducer 的例子,它的主要作用是使任意 reducer 变 成可以执行撤销和重做的全新 reducer。我们来看看它的核心代码实现:

function undoable(reducer) {
const initialState = {
// 记录过去的 state
past: [],
// 以一个空的 action 调用 reducer 来产生当前值的初始值
present: reducer(undefined, {}),
// 记录后续的 state
future: []
}
return function (state = initialState, action) {
const { past, present, future } = state
switch (action.type) {
case '@@redux-undo/UNDO':
const previous = past[past.length - 1]
const newPast = past.slice(0, past.length - 1)
return {
past: newPast,
present: previous,
future: [present, ...future]
}
case '@@redux-undo/REDO':
const next = future[0]
const newFuture = future.slice(1)

return {
past: [...past, present],
present: next,
future: newFuture
}
default:
// 将其他 action 委托给原始的 reducer 处理
const newPresent = reducer(present, action)
if (present === newPresent) {
return state
}
return {
past: [...past, present],
present: newPresent,
future: []
}
}
}
}

有了这个高阶 reducer,就可以对任意一个 reducer 进行封装:

import { createStore } from 'redux'
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
// ...
}
}
const undoableTodos = undoable(todos)
const store = createStore(undoableTodos)
store.dispatch({ type: 'ADD_TODO', text: 'Use Redux' })
store.dispatch({
type: 'ADD_TODO',
text: 'Implement Undo'
})
store.dispatch({
type: '@@redux-undo/UNDO'
})

查看高阶 reducer undoable 的实现代码可以发现,高阶 reducer 主要通过下面 3 点来增强reducer:

  • 能够处理额外的 action;
  • 能够维护更多的 state;
  • 将不能处理的 action 委托给原始 reducer 处理。

Redux 与表单

后续再看

Redux CRUD 实战

后续再看

Redux 性能优化

后续再看