我应该看这篇文章吗?
我认为,如果您是:
- 尝试减少Redux样板;要么
- 对改善Redux架构或文件结构感兴趣;要么
- 尝试将Redux操作作为“命令”与“事件”进行导航。
关键要点在这篇文章的底部。
我最近观看了Yazan Alaboudi的精彩演讲的录音,“我们的Redux反模式:可预测的可伸缩性指南”(在此处幻灯片)。我真的很喜欢听到和阅读人们对Redux体系结构的想法,这是我已经考虑了很多的东西。
在演讲中,Yazan提供了两个很好的案例:
- 将Redux操作作为命令编写1是一种反模式;和
- 精心编写的Redux动作应代表业务事件。
在这篇特别的文章中,我将回应其中的第一点,以期在另一篇文章中讨论第二点。
在这里,我的核心论点是:Redux-Leaves解决了Yazan对命令行动的“反模式”批评中的大部分(也许全部)。
我将分为两个部分:
- 首先,我将概述Yazan反对指挥行动的案例;和
- 其次,我将演示Redux-Leaves如何解决这些问题。
Yazan反对命令行动的情况是什么?
我建议您看一下Yazan自己的解释,但在下面,我将概述我对他所说的话的解释。
范例程式码
命令操作示例(Redux设置)
// in scoreboardReducer.js
const INITIAL_STATE = {
home: 0,
away: 0
};
function scoreboardReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "INCREMENT_SCORE": {
const scoringSide = action.payload;
return { ...state, [scoringSide]: state[scoringSide] + 1};
}
default: return state;
}
}
//in crowdExcitmentReducer.js
const INITIAL_STATE = 0;
function crowdExcitementReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case "INCREASE_CROWD_EXCITEMENT": return state + 1;
default: return state;
}
}
命令操作后果(组件分派)
// in GameComponent
class GameComponent extends React.Component {
scoreGoal() {
dispatch({ type: "INCREMENT_SCORE", scoringSide: "home"});
dispatch({ type: "INCREASE_CROWD_EXCITEMENT"});
// potentially more dispatches
}
render() {
//...
}
}
然后在一个关键幻灯片中,他列出了在这些面向命令的示例中看到的一些成本:
指挥行动的缺点
- 没有捕获业务语义
- 动作与减速器相连
- 动作太多
- 不清楚为什么状态在改变
- 导致很多样板
- 不缩放
这是我对上述每一项的观察(“业务语义”除外,我将在另一篇文章中讨论):
动作与减速器耦合
我认为,在谈到Yazan提供的示例代码时,非常公平地注意到动作与缩减器相关。所述"INCREMENT_SCORE"
动作类型看起来完全耦合到scoreboardReducer
,并且"INCREASE_CROWD_EXCITEMENT"
看起来完全耦合到crowdExcitementReducer
。
这不是一个好的模式,因为这意味着我们的代码可重用性极低。如果我想增加其他内容,例如体育场观众人数,则我需要使用另一种动作类型"INCREMENT_AUDIENCE_SIZE"
,即使状态变化会非常相似。
动作太多
同样,当涉及到Yazan的示例代码,我认为这是公平地注意到,更多的动作在分派scoreGoal
该功能感觉必要的。如果已达成目标,那么一件事情就已经发生了,但是我们正在触发多个动作。
这不是一个好方法,因为它将使Redux DevTools阻塞很多噪音,并可能导致Redux状态多次更新而不是进行一次大的更新,从而导致不必要的重新渲染。
不清楚为什么状态在改变
我不认为这是个大问题。对我来说,在Yazan的示例代码中,将链接从和链接scoreGoal
到我并不难。"INCREASE_SCORE"
"INCREASE_CROWD_EXCITEMENT"
就不清楚“为什么”的程度而言,我认为可以通过注释更好的代码来解决-这不是Redux中命令操作所特有的情况,而是适用于所有命令式代码的情况。
导致很多样板/不缩放
我认为这两个都是合法的关注点(并且从本质上讲,是相同的关注点):如果每次我们决定要实现状态的新变化时,我们都必须决定新的操作类型并实现一些新的reducer逻辑,我们将快速了解与Redux相关的代码,这意味着它不能很好地扩展。
Redux-Leaves如何解决这些问题?
首先,让我们看一些与前面的示例等效的示例代码:
Redux-Leaves设置
// store.js
import { createStore } from 'redux'
import reduxLeaves from 'redux-leaves'
const initialState = {
crowdExcitment: 0,
scoreboard: {
home: 0,
away: 0
}
}
const [reducer, actions] = reduxLeaves(initialState)
const store = createStore(reducer)
export { store, actions }
组件分派
// in GameComponent
import { bundle } from 'redux-leaves'
import { actions } from './path/to/store'
class GameComponent extends React.Component {
scoreGoal() {
// create and dispatch actions to increment both:
// * storeState.scoreboard.home
// * storeState.crowdExcitement
dispatch(bundle([
actions.scoreboard.home.create.increment(),
actions.crowdExcitement.create.increment()
// potentially more actions
]));
}
render() {
//...
}
}
这是一个带有类似代码的交互式RunKit游乐场,供您测试和试验。
希望,与Yazan给出的更典型的命令操作示例相比,这种Redux-Leaves设置可以说明一切:
- 只有一个初始状态和减速器要处理
- 不再需要手动编写减速器
- 无需您手动手动编写案例逻辑
现在,我还将介绍它如何解决上述每个特定问题:
动作不再与减速器耦合
Redux-Leaves为您提供了一个increment
开箱即用actions
的动作创建者,可用于的任意状态路径。
增加… | create 而dispatch 这个动作… |
---|---|
storeState.crowdExcitement |
actions.crowdExcitement.create.increment() |
storeState.scoreboard.away |
actions.scoreboard.away.create.increment() |
storeState.scoreboard.home |
actions.scoreboard.home.create.increment() |
您还会得到一大堆其他默认操作创建者,这些创建者都可以在状态树的任意叶子上实现。
动作太多?将它们捆绑成一个
Redux-Leaves有一个命名的bundle
导出,它接受Redux-Leaves创建的一系列动作,并返回一个可以在单个调度中影响所有这些更改的动作。
import { createStore } from 'redux'
import reduxLeaves, { bundle } from 'redux-leaves'
const initialState = {
crowdExcitment: 0,
scoreboard: {
home: 0,
away: 0
}
}
const [reducer, actions] = reduxLeaves(initialState)
const store = createStore(reducer)
store.getState()
/*
{
crowdExcitement: 0,
scoreboard: {
home: 0,
away: 0
}
}
*/
store.dispatch(bundle([
actions.scoreboard.home.create.increment(7),
actions.scoreboard.away.create.increment(),
actions.crowdExcitement.create.increment(9001)
]))
store.getState()
/*
{
crowdExcitement: 9001,
scoreboard: {
home: 7,
away: 1
}
}
*/
非常清楚地了解正在改变的状态
在Yazan的命令操作示例中,尚不清楚整体存储状态将如何受到调度的影响-哪个得分在哪个状态位递增?
使用Redux-Leaves,actions
API意味着您可以非常清楚地知道要更改的状态:您使用属性路径指向要在其上创建操作的状态,就像查看状态树并描述时一样。您想影响哪个状态。
(这不是解决相当的相同点Yazan做,我认为这是在问,“但为什么是我们增加的人群激动呢?” -但是,正如我在表明在讨论这一点,我认为这是开发商作出的责任必要时通过注释清除命令的原因。)
极少的样板
这是获取根减少器和动作创建者所需执行的操作:
import reduxLeaves from 'redux-leaves'
const [reducer, actions] = reduxLeaves(initialState)
而已。两行,其中之一是导入。无需case
亲自编写动作常量,创建者或化简语句。
易于扩展
假设我们要引入某种状态来跟踪团队名称。
我们要做的就是改变我们的初始状态。
import reduxLeaves from 'redux-leaves'
const initialState = {
crowdExcitement: 0,
scoreboard: {
home: 0,
away: 0
},
+ teams: {
+ home: 'Man Red',
+ away: 'Man Blue'
}
}
const [reducer, actions] = reduxLeaves(initialState)
const store = createStore(reducer)
…然后我们可以立即开始调度动作以更新该状态,而无需编写任何其他的reducer,动作创建者或动作类型:
store.dispatch(actions.teams.away.create.update('London Blue'))
store.getState().teams.away // => 'London Blue'
外卖
- 您应该观看Yazan的讲话并查看他的幻灯片,两者都非常周到
- Yazan 强烈反对通常如何编写Redux命令操作
- 我认为Redux-Leaves 解决了大多数问题,即使不是全部
尾注
1如果Redux动作表示要执行某项操作,则可以将其视为命令。Yazan给出的示例是:
{ type: 'SEND_CONFIRMATION' }
{ type: 'START_BILLING' }
{ type: 'SEND_LETTER' }
{ type: 'INCREMENT_SCORE' }
{ type: 'INCREASE_CROWD_EXCITEMENT' }