Redux: 关于动作创建器、减速器和选择器的最佳实践建议

创建于 2015-12-22  ·  106评论  ·  资料来源: reduxjs/redux

我的团队已经使用 Redux 几个月了。 在此过程中,我偶尔会发现自己在考虑一个功能并想知道“这属于动作创建器还是减速器?”。 文档在这个事实上似乎有点含糊。 (或者也许我只是错过了它所涵盖的地方,在这种情况下,我道歉。)但是随着我编写了更多的代码和更多的测试,我对事情应该在哪里有更强烈的看法,我认为这是值得的与他人分享和讨论。

所以这是我的想法。

到处使用选择器

第一个与 Redux 没有严格的关系,但无论如何我都会分享它,因为它在下面间接提到。 我的团队使用rackt/reselect 。 我们通常定义一个文件,该文件为状态树的给定节点(例如 MyPageSelectors)导出选择器。 我们的“智能”容器然后使用这些选择器来参数化我们的“哑”组件。

随着时间的推移,我们意识到在其他地方(不仅仅是在重新选择的上下文中)使用这些相同的选择器会带来额外的好处。 例如,我们在自动化测试中使用它们。 我们还在动作创建者返回的thunk 中使用它们(更多内容见下文)。

所以我的第一个建议是 - 使用共享选择器 _everywhere_- 即使在同步访问数据时(例如,比state.myValue更喜欢myValueSelector(state) state.myValue )。 这减少了导致细微未定义值的错误输入变量的可能性,它简化了对商店结构的更改等。

在 action-creators 中做 _more_,在 reducer 中做 _less_

我认为这一点非常重要,尽管它可能不会立即显而易见。 业务逻辑属于动作创建者。 Reducers 应该是愚蠢而简单的。 在许多个别情况下,这并不重要 - 但一致性很好,所以最好_一致地_这样做。 有几个原因:

  1. 动作创建者可以通过使用像redux-thunk这样的中间件来异步。 由于您的应用程序通常需要对您的商店进行异步更新 - 一些“业务逻辑”最终会出现在您的操作中。
  2. 动作创建者(更准确地说是他们返回的thunk )可以使用共享选择器,因为他们可以访问完整的状态。 Reducers 不能,因为他们只能访问他们的节点。
  3. 使用redux-thunk ,单个动作创建者可以分派多个动作——这使得复杂的状态更新更简单,并鼓励更好的代码重用。

想象一下,您的状态具有与项目列表相关的元数据。 每次修改、添加到列表或从列表中删除项目时,都需要更新元数据。 保持列表及其元数据同步的“业务逻辑”可能存在于以下几个地方:

  1. 在减速机中。 每个reducer(添加、编辑、删除)负责更新列表_以及_元数据。
  2. 在视图(容器/组件)中。 每个调用操作(添加、编辑、删除)的视图还负责调用updateMetadata操作。 由于(希望)显而易见的原因,这种方法很糟糕。
  3. 在行动创造者中。 每个动作创建者(添加、编辑、删除)都会返回一个thunk ,它调度一个动作来更新列表,然后另一个动作来更新元数据。

鉴于上述选择,选项 3 确实更好。 选项 1 和 3 都支持干净的代码共享,但只有选项 3 支持列表和/或元数据更新可能是异步的情况。 (例如,它可能依赖于网络工作者。)

编写专注于操作和选择器的“鸭子”测试

测试动作、reducer 和选择器的最有效方法是在编写测​​试时遵循“鸭子”方法。 这意味着您应该编写一组测试来涵盖一组给定的操作、reducer 和选择器,而不是 3 组单独关注每个的测试。 这更准确地模拟了实际应用程序中发生的情况,并提供了最大的收益。

进一步细分,我发现编写专注于动作创建者的测试然后使用选择器验证结果很有用。 (不要直接测试reducer。)重要的是给定的动作会导致你期望的状态。 使用您的(共享)选择器验证此结果是一种一次性涵盖所有三个的方法。

discussion

最有用的评论

@dtinth @denis-sokolov 我也同意你的看法。 顺便说一句,当我提到redux-saga项目时,我可能没有明确表示我反对让 actionCreators 随着时间的推移不断增长并变得越来越复杂的想法。

Redux-saga 项目也是尝试做您所描述的@dtinth,但与您所说的存在

也许你可以看看导致 Redux saga 讨论的原始讨论的这一点: https :

用例解决

想象一下,您有一个 Todo 应用程序,带有明显的 TodoCreated 事件。 然后我们要求您编写一个应用程序引导程序。 一旦用户创建了一个待办事项,我们应该用一个弹出窗口祝贺他。

“不纯”的方式:

这就是@bvaughn似乎更喜欢的

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

我不喜欢这种方法,因为它使动作创建者与应用程序视图的布局高度耦合。 它假设 actionCreator 应该知道 UI 状态树的结构来做出决定。

“从原始事件计算一切”方式:

这是@denis-sokolov @dtinth似乎更喜欢的:

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

是的,您可以创建一个减速器,它知道是否应该显示祝贺。 但是,您将显示一个弹出窗口,甚至没有任何操作表明已显示该弹出窗口。 根据我自己的经验(并且仍然有遗留代码这样做),最好让它非常明确:如果没有操作 DISPLAY_CONGRATULATION 被触发,永远不要显示祝贺弹出窗口。 显式比隐式更容易维护。

简化的传奇方式。

redux-saga 使用生成器,如果您不习惯,可能看起来有点复杂,但基本上使用简化的实现,您可以编写如下内容:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

saga 是一个有状态的actor,它接收事件并可能产生效果。 在这里它被实现为一个不纯的减速器,让你了解它是什么,但它实际上不在 redux-saga 项目中。

稍微复杂一点的规则:

如果您注意最初的规则,那么它对所有内容都不是很明确。
如果您查看上述实现,您会注意到每次我们在入职期间创建待办事项时都会打开祝贺弹出窗口。 最有可能的是,我们希望它仅针对在入职期间发生的第一个创建的待办事项而不是所有待办事项打开。 此外,我们希望允许用户最终从头开始重做引导。

随着时间的推移,随着入门变得越来越复杂,您能看到所有 3 个实现中的代码如何变得混乱吗?

redux-saga 方式

使用 redux-saga 和上面的入职规则,你会写一些类似的东西

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

我认为它以比上述解决方案更简单的方式解决了这个用例。 如果我错了,请给我你更简单的实现:)

您谈到了不纯代码,在这种情况下,Redux-saga 实现中没有不纯内容,因为 take/put 效果实际上是数据。 当 take() 被调用时,它不执行,它返回一个要执行的效果的描述符,并且在某个时候解释器启动,所以你不需要任何模拟来测试传奇。 如果您是使用 Haskell 的函数式开发人员,请考虑 Free / IO monads。


在这种情况下,它允许:

  • 避免使 actionCreator 复杂化并使其依赖于getState
  • 让隐含的更显化
  • 避免将横向逻辑(如上面的入职)耦合到您的核心业务领域(创建待办事项)

它还可以提供一个解释层,允许将原始事件转换为更有意义/更高级的事件(有点像 ELM 通过在事件冒泡时包装事件)。

例子:

  • “TIMELINE_SCROLLED_NEAR-BOTTOM”可能会导致“NEXT_PAGE_LOADED”
  • 如果错误代码为 401,“REQUEST_FAILED”可能会导致“USER_DISCONNECTED”。
  • “HASHTAG_FILTER_ADDED”可能导致“CONTENT_RELOADED”

如果你想用鸭子实现模块化的应用程序布局,它可以避免将鸭子耦合在一起。 传奇成为耦合点。 鸭子只需要知道他们的原始事件,传奇就会解释这些原始事件。 这比让duck1 直接调度duck2 的动作要好得多,因为它使duck1 项目更容易在另一个上下文中重用。 然而,人们可能会争辩说,耦合点也可能在 actionCreators 中,这就是今天大多数人正在做的事情。

所有106条评论

好奇你是否使用 Immutable.js 或其他。 在我构建的少数 redux 东西中,我无法想象使用 immutable,但我_确实_有一个非常深的嵌套结构,Immutable 有助于驯服。

哇。 _不_提及这一点对我来说是多么的疏忽。 是的! 我们使用不可变的! 正如你所说,很难想象_不_将它用于任何实质性的事情。

@bvaughn我一直在努力的一个领域是在不可变和组件之间划清界限。 将不可变对象传递到组件中可以让您非常轻松地使用纯渲染装饰器/混合器,但最终您的组件中会出现 IMmutable 代码(我不喜欢)。 到目前为止,我只是屈服并完成了,但我怀疑您在 render() 方法中使用了选择器,而不是直接访问 Immutable.js 的方法?

老实说,这是我们尚未定义硬性政策的事情。 通常我们在“智能”容器中使用选择器从不可变对象中获取额外的原生值,然后将原生值作为字符串、布尔值等传递给我们的组件。偶尔我们会传递一个不可变对象,但当我们这样做时 - 我们几乎总是传递Record类型,以便组件可以将其视为本机对象(使用 getter)。

我一直在朝着相反的方向前进,让动作创作者变得更加琐碎。 但我真的只是从 redux 开始。 关于你的方法的一些问题:

1)你如何测试你的动作创建者? 我喜欢将尽可能多的逻辑转移到不依赖外部服务的纯同步函数中,因为这样更容易测试。
2)您是否使用带热重载的时间旅行? 使用 react redux devtools 的一项巧妙之处在于,当设置热重载时,商店将针对新的减速器重新运行所有操作。 如果我将我的逻辑移到动作创建器中,我就会失去它。
3)如果你的action creators多次调度带来一个效果,那是否意味着你的状态暂时处于无效状态? (我在这里考虑的是多个同步调度,而不是稍后异步调度)

到处使用选择器

是的,这似乎是在说您的减速器是您状态的实现细节,并且您通过查询 API 将您的状态公开给您的组件。
像任何接口一样,它允许解耦并使状态重构变得容易。

使用 ImmutableJS

IMO 使用新的 JS 语法,使用 ImmutableJS 不再有用,因为您可以使用普通 JS 轻松修改列表和对象。 除非您有非常大的列表和具有大量属性的对象,并且出于性能原因需要结构共享 ImmutableJS 不是一个严格的要求。

在行动中做得更多

@bvaughn你真的应该看看这个项目: https :
当我开始向@yelouafi讨论传奇(最初是后端概念)时,它是为了解决此类问题。 在我的情况下,我首先尝试在插入用户登录现有应用程序时使用 sagas。

1)你如何测试你的动作创建者? 我喜欢将尽可能多的逻辑转移到不依赖外部服务的纯同步函数中,因为这样更容易测试。

我试图在上面描述这一点,但基本上......我认为用“鸭子”式的方法测试你的动作创建者是最有意义的(到目前为止对我来说)。 通过分派 action-creator 的结果开始测试,然后使用选择器验证状态。 通过这种方式 - 只需一个测试,您就可以覆盖 action-creator、它的 reducer 和所有相关的选择器。

2)您是否使用带热重载的时间旅行? 使用 react redux devtools 的一项巧妙之处在于,当设置热重载时,商店将针对新的减速器重新运行所有操作。 如果我将我的逻辑移到动作创建器中,我就会失去它。

不,我们不使用时间旅行。 但是为什么您的业务逻辑在 action-creator 中会有任何影响呢? 更新您的应用程序状态的唯一东西是您的减速器。 因此,无论哪种方式,重新运行创建的操作都会获得相同的结果。

3)如果你的action creators多次调度带来一个效果,那是否意味着你的状态暂时处于无效状态? (我在这里考虑的是多个同步调度,而不是稍后异步调度)

在某些情况下,瞬态无效状态是您无法真正避免的。 只要有最终的一致性,那么它通常不是问题。 再说一次,无论您的业务逻辑是在动作创建器还是减速器中,您的状态都可能暂时无效。 它更多地与副作用和商店的细节有关。

IMO 使用新的 JS 语法,使用 ImmutableJS 不再有用,因为您可以使用普通 JS 轻松修改列表和对象。 除非您有非常大的列表和具有大量属性的对象,并且出于性能原因需要结构共享 ImmutableJS 不是一个严格的要求。

使用 Immutable(在我看来)的主要原因不是性能或更新的语法糖。 主要原因是它可以防止您(或其他人)_意外_ 在减速器中改变您的传入状态。 这是一个禁忌,不幸的是,使用普通的 JS 对象很容易做到。

@bvaughn你真的应该看看这个项目: https :
当我开始向@yelouafi讨论传奇(最初是后端概念)时,它是为了解决此类问题。 在我的情况下,我首先尝试在插入用户登录现有应用程序时使用 sagas。

我之前实际上已经检查过该项目 :) 虽然我还没有使用它。 它看起来很整洁。

我试图在上面描述这一点,但基本上......我认为用“鸭子”式的方法测试你的动作创建者是最有意义的(到目前为止对我来说)。 通过分派 action-creator 的结果开始测试,然后使用选择器验证状态。 通过这种方式 - 只需一个测试,您就可以覆盖 action-creator、它的 reducer 和所有相关的选择器。

对不起,我得到了那部分。 我想知道的是与异步性交互的测试部分。 我可能会写一个这样的测试:

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

但是你的测试是什么样的? 像这样的东西?

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

不,我们不使用时间旅行。 但是为什么您的业务逻辑在 action-creator 中会有任何影响呢? 更新您的应用程序状态的唯一东西是您的减速器。 因此,无论哪种方式,重新运行创建的操作都会获得相同的结果。

如果代码改了怎么办? 如果我更改减速器,相同的动作会重播,但使用新的减速器。 而如果我更改动作创建者,则不会重播新版本。 所以要考虑两种情况:

带减速机:

1) 我在我的应用程序中尝试了一个操作。
2)我的reducer有bug,导致状态错误。
3)我修复了减速器中的错误并保存
4)时间旅行加载了新的减速器并使我处于我应该处于的状态。

而对于动作创建者

1) 我在我的应用程序中尝试了一个操作。
2)action creator存在bug,导致创建的action错误
3)我修复了动作创建器中的错误并保存
4)我仍然处于不正确的状态,这将要求我至少再次尝试该操作,如果它使我处于完全崩溃的状态,则可能会刷新。

在某些情况下,瞬态无效状态是您无法真正避免的。 只要有最终的一致性,那么它通常不是问题。 再说一次,无论您的业务逻辑是在动作创建器还是减速器中,您的状态都可能暂时无效。 它更多地与副作用和商店的细节有关。

我想我对 redux 的看法坚持认为 store 始终处于有效状态。 减速器总是采用有效状态并产生有效状态。 你认为哪些情况会迫使人们允许一些不一致的状态?

抱歉打扰了,但是无效和有效状态是什么意思
这里? 正在加载数据或正在执行动作但尚未完成的样子
对我来说就像一个有效的瞬态。

Redux @bvaughn@sompylasar 中的瞬态状态是什么意思? 要么调度完成,要么抛出。 如果抛出则状态不会改变。

除非你的 reducer 有代码问题,否则 Redux 只有与 reducer 逻辑一致的状态。 以某种方式调度的所有操作都在事务中处理:要么整个树更新,要么状态根本不改变。

如果整个树更新但没有以适当的方式更新(例如 React 无法呈现的状态),那只是您没有正确完成工作:)

在 Redux 中,当前状态是将单个分派视为事务边界。

但是我理解@winstonewert的担忧,它似乎想要在同一个事务中同步分派 2 个动作。 因为有时 actionCreators 调度多个动作并期望所有动作都正确执行。 如果分派了 2 个动作,然后第二个动作失败,那么只会应用第一个动作,导致我们可以认为是“不一致”的状态。 也许@winstonewert希望如果第二个动作调度失败,那么我们回滚这两个动作。

@winstonewert我已经在我们的内部框架中实现了类似的东西,直到现在它都运行良好: https :
我还想处理渲染错误:如果无法成功渲染状态,我希望回滚我的状态以避免阻塞 UI。 不幸的是,直到下一个版本,当渲染方法抛出错误时,React 的工作非常糟糕,所以它没有那么有用,但可能在未来有用。

我很确定我们可以允许商店在具有中间件的事务中接受多个同步分派。

但是,我不确定在呈现错误的情况下是否可以回滚状态,因为通常当我们尝试呈现其状态时,redux 存储已经“提交”了。 在我的框架中有一个“beforeTransactionCommit”钩子,我用它来触发渲染并最终回滚任何渲染错误。

@gaearon我想知道您是否打算支持这些类型的功能,以及当前的 API 是否有可能。

在我看来, redux-batched-subscribe不允许进行真正的交易,而只是减少了渲染次数。 我看到的是,即使订阅侦听器在最后只触发一次,商店在每次调度后“提交”

为什么我们需要完整的交易支持? 我不认为我理解用例。

@gaearon我还不确定,但很高兴了解更多@winstonewert 用例。

这个想法是你可以做dispatch([a1,a2])并且如果 a2 失败,那么我们回滚到 a1 被分派之前的状态。

过去,我经常同步调度多个动作(例如在单个 onClick 侦听器上,或在 actionCreator 中),并且主要实现事务作为仅在调度的所有动作结束时调用渲染的一种方式,但这一直是redux-batched-subscribe 项目以不同的方式解决了这个问题。

在我的用例中,我过去在事务上触发的操作主要是为了避免不必要的渲染,但这些操作确实独立有意义,因此即使第二个操作的调度失败,不回滚第一个操作仍然会给我一个一致的状态(但也许不是计划中的那个......)。 我真的不知道是否有人可以提出一个完全回滚有用的用例

但是,当渲染失败时,尝试回滚到渲染不会失败的最后一个状态而不是尝试在无法渲染的状态上取得进展是否有意义?

一个简单的减速器增强器会起作用吗? 例如

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

有了它,您就可以将数组分派到商店。 增强器解包单个动作并将其提供给每个减速器。

是的,它存在: https :

@gaearon我不知道。 我没有注意到有 2 个不同的项目试图以不同的方式解决一个非常相似的用例:

两者都可以避免不必要的渲染,但第一个会回滚所有批处理操作,而第二个只会不应用失败的操作。

@gaearon哎哟,我很遗憾没有看那个。 :酡:


Action Creators 代表不纯代码

我可能没有像大多数人那样有多少 Redux 的实践经验,但乍一看,我不得不不同意“在动作创建者中做更多,在减速器中做更少”,我在我们的内部进行了一些类似的讨论。公司。

在引入 Flux 模式的Hacker Way: Rethinking Web App Development at Facebook中,导致 Flux 被发明的问题正是命令式代码。

在这种情况下,执行 I/O 的 Action Creator 就是命令式代码。

我们没有在工作中使用 Redux,但在我工作的地方,我们曾经有细粒度的操作(当然,这一切都是有意义的)并批量触发它们。 例如,当您单击一条消息时,会触发三个操作: OPEN_MESSAGE_VIEWFETCH_MESSAGEMARK_NOTIFICATION_AS_READ

然后事实证明,这些“低级”操作只不过是在 store 中设置某个值的“命令”或“setter”或“消息”。 如果我们继续这样做,我们不妨回去使用 MVC 并最终得到更简单的代码。

从某种意义上说, Action Creators 代表不纯代码,而 Reducers(和 Selectors)代表纯代码。 Haskell 人已经发现,不纯代码越少,代码越纯越好

例如,在我的副项目(使用 Redux)中,我使用了 webkit 的语音识别 API。 它会在您说话时发出onresult事件。 有两种选择——这些事件在哪里得到处理?

  • 让动作创建者先处理事件,然后将其发送到商店。
  • 只需将事件对象发送到商店即可。

我选择了第二个:只需将原始事件对象发送到商店。

但是,Redux dev-tools 似乎不喜欢将非纯对象发送到 store 中,因此我在 action creator 中添加了一些微小的逻辑,将这些事件对象转换为纯对象。 (动作创建器中的代码非常简单,不会出错。)

然后,reducer 可以组合这些非常原始的事件并构建所说内容的转录本。 因为该逻辑存在于纯代码中,所以我可以很容易地对其进行实时调整(通过热重载减速器)。

我想支持@dtinth。 动作应该代表发生在现实世界中的事件,而不是我们想要如何对这些事件做出反应。 特别是,请参阅 CQRS:我们希望记录有关现实生活事件的尽可能多的详细信息,并且可能会在将来改进减速器并使用新逻辑处理旧事件。

@dtinth @denis-sokolov 我也同意你的看法。 顺便说一句,当我提到redux-saga项目时,我可能没有明确表示我反对让 actionCreators 随着时间的推移不断增长并变得越来越复杂的想法。

Redux-saga 项目也是尝试做您所描述的@dtinth,但与您所说的存在

也许你可以看看导致 Redux saga 讨论的原始讨论的这一点: https :

用例解决

想象一下,您有一个 Todo 应用程序,带有明显的 TodoCreated 事件。 然后我们要求您编写一个应用程序引导程序。 一旦用户创建了一个待办事项,我们应该用一个弹出窗口祝贺他。

“不纯”的方式:

这就是@bvaughn似乎更喜欢的

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

我不喜欢这种方法,因为它使动作创建者与应用程序视图的布局高度耦合。 它假设 actionCreator 应该知道 UI 状态树的结构来做出决定。

“从原始事件计算一切”方式:

这是@denis-sokolov @dtinth似乎更喜欢的:

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

是的,您可以创建一个减速器,它知道是否应该显示祝贺。 但是,您将显示一个弹出窗口,甚至没有任何操作表明已显示该弹出窗口。 根据我自己的经验(并且仍然有遗留代码这样做),最好让它非常明确:如果没有操作 DISPLAY_CONGRATULATION 被触发,永远不要显示祝贺弹出窗口。 显式比隐式更容易维护。

简化的传奇方式。

redux-saga 使用生成器,如果您不习惯,可能看起来有点复杂,但基本上使用简化的实现,您可以编写如下内容:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

saga 是一个有状态的actor,它接收事件并可能产生效果。 在这里它被实现为一个不纯的减速器,让你了解它是什么,但它实际上不在 redux-saga 项目中。

稍微复杂一点的规则:

如果您注意最初的规则,那么它对所有内容都不是很明确。
如果您查看上述实现,您会注意到每次我们在入职期间创建待办事项时都会打开祝贺弹出窗口。 最有可能的是,我们希望它仅针对在入职期间发生的第一个创建的待办事项而不是所有待办事项打开。 此外,我们希望允许用户最终从头开始重做引导。

随着时间的推移,随着入门变得越来越复杂,您能看到所有 3 个实现中的代码如何变得混乱吗?

redux-saga 方式

使用 redux-saga 和上面的入职规则,你会写一些类似的东西

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

我认为它以比上述解决方案更简单的方式解决了这个用例。 如果我错了,请给我你更简单的实现:)

您谈到了不纯代码,在这种情况下,Redux-saga 实现中没有不纯内容,因为 take/put 效果实际上是数据。 当 take() 被调用时,它不执行,它返回一个要执行的效果的描述符,并且在某个时候解释器启动,所以你不需要任何模拟来测试传奇。 如果您是使用 Haskell 的函数式开发人员,请考虑 Free / IO monads。


在这种情况下,它允许:

  • 避免使 actionCreator 复杂化并使其依赖于getState
  • 让隐含的更显化
  • 避免将横向逻辑(如上面的入职)耦合到您的核心业务领域(创建待办事项)

它还可以提供一个解释层,允许将原始事件转换为更有意义/更高级的事件(有点像 ELM 通过在事件冒泡时包装事件)。

例子:

  • “TIMELINE_SCROLLED_NEAR-BOTTOM”可能会导致“NEXT_PAGE_LOADED”
  • 如果错误代码为 401,“REQUEST_FAILED”可能会导致“USER_DISCONNECTED”。
  • “HASHTAG_FILTER_ADDED”可能导致“CONTENT_RELOADED”

如果你想用鸭子实现模块化的应用程序布局,它可以避免将鸭子耦合在一起。 传奇成为耦合点。 鸭子只需要知道他们的原始事件,传奇就会解释这些原始事件。 这比让duck1 直接调度duck2 的动作要好得多,因为它使duck1 项目更容易在另一个上下文中重用。 然而,人们可能会争辩说,耦合点也可能在 actionCreators 中,这就是今天大多数人正在做的事情。

@slorber这是一个很好的例子! 感谢您花时间清楚地解释每种方法的优缺点。 (我什至认为这应该放在文档中。)

我曾经探索过一个类似的想法(我将其命名为“工人组件”)。 基本上,它是一个不渲染任何东西( render: () => null )的 React 组件,但会监听事件(例如来自商店)并触发其他副作用。 然后将该工作组件放入应用程序根组件中。 只是另一种处理复杂副作用的疯狂方式。 :stuck_out_tongue:

我睡觉的时候在这里讨论了很多。

@winstonewert ,你提出了一个关于时间旅行和重放错误代码的好观点。 我认为某些类型的错误/更改无论如何都不适用于时间旅行,但我认为总体而言您是对的。

@dtinth ,对不起,但我没有关注你的大部分评论。 你的 action-creator/reducer “ducks” 代码的某些部分必须是不纯的,因为它的某些部分必须获取数据。 除此之外,你失去了我。 我最初的帖子的主要目的之一就是实用主义。


@winstonewert说:“我想我对 redux 的看法坚持认为 store 始终处于有效状态。”
@slorber问道,“你在 Redux @bvaughn@sompylasar 中的瞬态状态是什么意思?要么调度完成,要么抛出。如果抛出,那么状态不会改变。”

我很确定我们正在考虑不同的事情。 当我说“瞬时无效状态”时,我指的是如下用例。 例如,我和一位同事最近发布了redux-search 。 此搜索中间件侦听可搜索事物集合的更改,然后(重新)索引它们以进行搜索。 如果用户提供过滤器文本,redux-search 将返回与用户文本匹配的资源 uid 列表。 因此,请考虑以下事项:

想象一下,您的应用程序商店包含一些可搜索的对象: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] 。 用户输入了过滤器文本“e”,因此搜索中间件包含一个 ID 为 1 和 3 的数组。现在假设用户 1 (Alex) 被删除 - 响应本地用户操作或刷新远程数据不再包含该用户记录。 当您的 reducer 更新用户集合时,您的商店将暂时无效——因为 redux-search 将引用集合中不再存在的 id。 一旦中间件再次运行,它将更正无效状态。 任何时候树的一个节点与另一个节点相关,这种事情都可能发生。


@slorber说,“我不喜欢这种方法,因为它使动作创建者与应用程序视图的布局高度耦合。它假设 actionCreator 应该知道 UI 状态树的结构来做出决定。”

我不明白你所说的将动作创建器“与应用程序视图的布局”耦合的方法是什么意思。 状态树_驱动_(或通知)UI。 这是 Flux 的主要目的之一。 根据定义,您的动作创建器和化简器与该状态(但不与 UI)相结合。

值得一提的是,您编写的示例代码作为我喜欢的东西并不是我想到的那种东西。 也许我没有很好地解释自己。 我认为讨论这样的事情的困难在于它通常不会在简单或常见的例子中表现出来。 (例如,标准的 TODO MVC 应用程序对于像这样的细微讨论来说不够复杂。)

为清楚起见,对最后一点进行了编辑

顺便说一句, @slorber这里是我想到的一个例子。 有点做作。

假设您的州有许多节点。 这些节点之一存储共享资源。 (“共享”是指在本地缓存并由应用程序中的多个页面访问的资源。)这些共享资源有自己的动作创建器和化简器(“鸭子”)。 另一个节点存储特定应用程序页面的信息。 您的页面也有自己的鸭子。

假设您的页面需要加载最新最好的事物,然后允许用户对其进行编辑。 这是我可能用于这种情况的示例操作创建器方法:

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

也许@winstonewert希望如果第二个动作调度失败,那么我们回滚这两个动作。

不。我不会编写一个分派两个动作的动作创建者。 我会定义一个做两件事的动作。 OP 似乎更喜欢分派较小动作的动作创建者,这允许我不喜欢的瞬时无效状态。

当您的 reducer 更新用户集合时,您的商店将暂时无效——因为 redux-search 将引用集合中不再存在的 id。 一旦中间件再次运行,它将更正无效状态。 任何时候树的一个节点与另一个节点相关,这种事情都可能发生。

这实际上是一种困扰我的案例。 在我看来,理想情况下,索引应该完全由减速器或选择器处理。 必须调度额外的操作来保持搜索最新似乎不太纯粹地使用 redux。

OP 似乎更喜欢分派较小动作的动作创建者,这允许我不喜欢的瞬时无效状态。

不完全是。 关于状态树的动作创建者节点,我更喜欢单个动作。 但是,如果单个概念性用户“操作”影响状态树的多个节点,则您需要分派多个操作。 您可以单独调用每个动作(我认为这是 _bad_),或者您可以让单个动作创建者分派动作(redux-thunk 方式,我认为这是 _better_,因为它从您的视图层隐藏了该信息)。

这实际上是一种困扰我的案例。 在我看来,理想情况下,索引应该完全由减速器或选择器处理。 必须调度额外的操作来保持搜索最新似乎不太纯粹地使用 redux。

你不是在调度额外的动作。 搜索是一个中间件。 它是自动的。 但是,当树的两个节点不一致时,确实存在瞬态。

@bvaughn哦,对不起,我是个纯粹主义者!

好吧,不纯代码与数据获取和其他副作用/IO 有关,而纯代码不能触发任何副作用。 有关纯代码和非纯代码之间的比较,请参见此表

Flux 最佳实践说一个动作应该“描述用户的动作,而不是 setter”。 Flux 文档还进一步暗示了这些操作应该来自哪里:

当新数据进入系统时,无论是通过与应用程序交互的人还是通过Web api 调用,该数据都会被打包成一个动作——一个包含新数据字段和特定动作类型的对象字面量。

基本上,动作是描述“发生了什么”而不是应该发生什么的事实/数据。 商店只能同步、可预测地对这些操作做出反应,并且没有任何其他副作用。 所有其他副作用都应该在动作创建者(或传奇 :wink:)中处理。

例子

我并不是说这是最好的方法或比任何其他方法更好,甚至不是一个好方法。 但这是我目前认为的最佳实践。

例如,假设用户想要查看需要连接到远程服务器的记分板。 这是应该发生的事情:

  • 用户点击查看记分牌按钮。
  • 记分板视图显示加载指示器。
  • 向服务器发送请求以获取记分板。
  • 等待回应。
  • 如果成功,则显示记分牌。
  • 如果失败,记分板将关闭并出现一个带有错误消息的消息框。 用户可以关闭它。
  • 用户可以关闭记分板。

假设动作只能作为用户动作或服务器响应的结果到达商店,我们可以创建 5 个动作。

  • SCOREBOARD_VIEW (由于用户单击查看记分板按钮)
  • SCOREBOARD_FETCH_SUCCESS (服务器成功响应的结果)
  • SCOREBOARD_FETCH_FAILURE (由于服务器的错误响应)
  • SCOREBOARD_CLOSE (用户点击关闭按钮的结果)
  • MESSAGE_BOX_CLOSE (由于用户单击消息框上的关闭按钮)

这5个动作足以处理上述所有要求。 可以看到前4个动作与任何“鸭子”无关。 每个动作只描述了外部世界发生了什么(用户想这样做,服务器说那样)并且可以被任何减速器消费。 我们也没有MESSAGE_BOX_OPEN动作,因为那不是“发生了什么”(尽管那是应该发生的)。

改变状态树的唯一方法是发出一个动作,一个描述发生了什么的对象Redux 的 README

他们与这些动作创作者粘在一起:

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

然后 store 的每个部分(由 reducers 管理)可以对这些动作做出反应:

| Store/Reducer 的一部分 | 行为 |
| --- | --- |
| 记分牌查看 | 将SCOREBOARD_VIEW上的可见性更新为 true,将SCOREBOARD_CLOSESCOREBOARD_FETCH_FAILURE上的可见性更新为 false |
| 记分板LoadingIndicator | 在SCOREBOARD_VIEW上将可见性更新为 true,在SCOREBOARD_FETCH_*上更新为 false |
| 记分牌数据 | 在SCOREBOARD_FETCH_SUCCESS上更新商店内的数据 |
| 留言框 | 将SCOREBOARD_FETCH_FAILURE上的可见性更新为 true 并存储消息,并将MESSAGE_BOX_CLOSE上的可见性更新为 false |

如您所见,单个操作可以影响商店的许多部分。 商店只得到一个动作(发生了什么?)的高级描述,而不是一个命令(做什么?)。 其结果:

  1. 更容易查明错误。

没有什么可以影响消息框的状态。 没有人可以告诉它以任何理由打开。 它只对订阅的内容(用户操作和服务器响应)做出反应。

例如,如果服务器无法获取记分板,并且没有出现消息框,则您无需找出未调度SHOW_MESSAGE_BOX操作的原因。 很明显,消息框没有正确处理SCOREBOARD_FETCH_FAILURE操作。

修复是微不足道的,可以热重载和时间旅行。

  1. 动作创建器和减速器可以分开测试。

您可以测试动作创建者是否正确描述了外部世界中发生的事情,而无需考虑商店对它们的反应。

以同样的方式,可以简单地测试 reducer 是否对来自外部世界的动作做出正确反应。

(集成测试仍然非常有用。)

不用担心。 :) 我感谢进一步的澄清。 听起来我们在这里达成了共识。 查看您的示例动作创建器viewScoreboard ,它看起来很像我的示例动作创建器fetchAndProcessThing ,就在它的正上方。

动作创建器和减速器可以分开测试。

虽然我同意这一点,但我认为将它们一起测试通常更实用。 很可能你的 action _or_ 或你的 reducer(也许两者)都非常简单,因此单独测试简单的操作的回报价值有点低。 这就是为什么我提议一起测试 action-creator、reducer 和相关选择器(作为“鸭子”)。

但是,如果单个概念性用户“操作”影响状态树的多个节点,则您需要分派多个操作。

这正是我认为您所做的与 redux 最佳实践不同的地方。 我认为标准的方法是让状态树的多个节点响应一个动作。

啊,有趣的观察@winstonewert。 我们一直遵循为每个“鸭子”包使用唯一类型常量的模式,因此通过扩展,reducer 仅响应由其兄弟动作创建者调度的动作。 老实说,我不确定我最初对任意减速器响应动作的感受。 感觉有点像坏封装。

我们一直遵循为每个“鸭子”包使用唯一类型常量的模式

请注意,我们不会在文档中的任何地方认可它 ;-) 并不是说​​它不好,但它给了人们某些关于 Redux 的有时错误的想法。

所以通过扩展,reducer 只响应由其兄弟 action-creators 分派的动作

Redux 中没有像 reducer / action creator 配对这样的东西。 这纯粹是鸭子的事情。 有些人喜欢它,但它掩盖了 Redux/Flux 模型的基本优势:状态突变彼此分离,并与导致它们的代码分离。

老实说,我不确定我最初对任意减速器响应动作的感受。 感觉有点像坏封装。

取决于您考虑的封装边界。 应用程序中的操作是全局的,我认为这很好。 由于复杂的产品需求,应用程序的一部分可能想要对另一部分的操作做出反应,我们认为这很好。 耦合是最小的:你所依赖的只是一个字符串和动作对象的形状。 好处是很容易在应用程序的不同部分引入动作的新派生,而无需与动作创建者建立大量的联系。 你的组件不知道当一个 action 被调度时到底发生了什么——这在 reducer 端决定。

所以我们官方的建议是,你应该首先尝试让不同的减速器响应相同的动作。 如果它变得尴尬,那么当然,制作单独的动作创建者。 但不要从这种方法开始。

我们确实推荐使用选择器——事实上,我们推荐导出从状态中读取的保持函数(“选择器”)和 reducer,并且总是在mapStateToProps使用它们,而不是在组件中硬编码状态结构。 这样很容易改变内部状态形状。 您可以(但不必)使用reselect来提高性能,但您也可以shopping-cart示例中那样天真地实现选择器。

也许,这归结为您是采用命令式风格还是反应式风格进行编程。 使用ducks 会导致actions 和reducer 高度耦合,从而鼓励更多的命令式action。

  • 在命令式风格中,商店给出“做什么”,例如SHOW_MESSAGE_BOXSHOW_ERROR
  • 在反应式风格中,商店被赋予“发生了什么的事实”,例如DATA_FETCHING_FAILEDUSER_ENTERED_INVALID_THING_ID 。 商店会做出相应的反应。

在前面的示例中,我没有SHOW_MESSAGE_BOX动作或showError('Invalid thing id="'+id+'"')动作创建者,因为这不是事实。 那是命令。

一旦该事实进入商店,您就可以将该事实转化为命令,在您的纯减速器中,例如

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

当一个动作作为“事实”而不是“命令”进入商店时,出错的可能性就较小,因为,嗯,这是一个事实。

现在,如果你的减速器误解了这个事实,它可以很容易地修复,并且修复可以穿越时间。 但是,如果您的动作创建者误解了这一事实,则您需要重新运行您的动作创建者。

您还可以更改减速器,以便在USER_ENTERED_INVALID_THING_ID触发时重置事物 ID 文本字段。 而这种变化也会穿越时空。 您还可以本地化您的错误消息,而无需刷新页面。 这收紧了反馈循环,并使调试和调整变得更加容易。

(我这里只说优点,当然也有缺点。考虑到你的商店只能同步响应这些事实并且没有副作用,你必须更多地考虑如何表达这个事实。参见讨论替代异步/副作用模型以及我在 StackOverflow 上发布的这个问题。我想我们还没有确定那部分。)


老实说,我不确定我最初对任意减速器响应动作的感受。 感觉有点像坏封装。

多个组件从同一个存储中获取数据也很常见。 单个组件依赖来自商店多个部分的数据也很常见。 这听起来是不是有点像糟糕的封装? 要成为真正的模块化,React 组件不应该也包含在“duck”包中吗? (榆树架构做到了这一点。)

React通过将来自商店的数据视为事实来

同样,我也相信 Redux/Flux 使您的数据模型具有反应性,通过将操作视为事实,因此您不必告诉您的数据模型如何自我更新。

感谢您抽出时间写下并分享您的想法,@dtinth。 还要感谢@gaearon参与本次讨论。 (我知道你有很多事情要做。)你们都给了我一些额外的考虑。 :)

多个组件从同一个存储中获取数据也很常见。 单个组件依赖来自商店多个部分的数据也很常见。 这听起来是不是有点像糟糕的封装?

呃...其中一些是主观的,但不是。 我将导出的动作创建器和选择器视为模块的 API。

无论如何,我认为这是一个很好的讨论。 就像 Thai 在他之前的回复中提到的那样,我们正在讨论的这些方法有利有弊。 深入了解其他方法真是太好了。 :)

顺便说一下,消息框是一个很好的例子,我希望在其中展示一个单独的动作创建者。 主要是因为我想在它创建时打发时间,以便它可以自动关闭(并且动作创建者是您调用不纯Date.now() ),因为我想设置一个计时器来关闭它,我想要去抖动那个计时器等等。所以我会考虑一个消息框,它的“动作流程”很重要,足以保证它的个人动作。 也就是说,也许我所描述的可以通过https://github.com/yelouafi/redux-saga更优雅地解决

我最初是在 Discord Reactiflux 聊天中写的,但被要求将其粘贴在这里。

我最近一直在思考同样的事情。 我觉得状态更新分为三个部分。

  1. 向动作创建者传递执行更新所需的最少信息量。 即任何可以从当前状态计算的东西都不应该在其中。
  2. 查询状态以获取完成更新所需的任何信息(例如,当您想复制 ID 为 X 的 Todo 时,您获取 ID 为 X 的 Todo 的属性,以便您可以进行复制)。 这可以在动作创建器中完成,然后该信息包含在动作对象中。 这会导致动作对象。 或者它可以在减速器中计算 -动作对象。
  3. 基于该信息,应用纯 reducer 逻辑来获取下一个状态。

现在,问题是在动作创建器中放入什么,在减速器中放入什么,在胖和瘦动作对象之间进行选择。 如果你把所有的逻辑都放在动作创建器中,你最终会得到基本上声明状态更新的胖动作对象。 Reducers 变得纯粹,愚蠢,添加这个,删除那个,更新这些功能。 它们将很容易组合。 但是您的业务逻辑并不多。

如果您在 reducer 中放置更多逻辑,您最终会得到漂亮、精简的 action 对象,大部分数据逻辑都在一个地方,但是您的 reducer 更难组合,因为您可能需要来自其他分支的信息。 您最终会得到大型减速器或减速器,它们从州的更高层获得额外的参数。

不知道这些问题的答案是什么,不知道现在有没有

感谢@tommikaikkonen 分享这些想法。 我自己仍然不确定这里的“答案”是什么。 我同意你的总结。 我会在“将所有逻辑放在动作创建器中...”部分添加一个小注释,它使您能够使用(共享)选择器来读取数据,这在某些情况下可能会很好。

这是一个有趣的话题! 弄清楚在 redux 应用程序中放置代码的位置是一个我想我们都面临的问题。 我确实喜欢 CQRS 的想法,即记录已经发生的事情。
但是我可以看到这里的想法不匹配,因为在 CQRS 中,AFAIK 最佳实践是从视图直接消耗的动作/事件中构建去规范化状态。 但是在 redux 中,最佳实践是构建一个完全规范化的状态并通过选择器导出视图的数据。
如果我们构建可由视图直接使用的非规范化状态,那么我认为一个 reducer 想要另一个 reducer 中的数据的问题就消失了(因为每个 reducer 可以只存储它不需要关心规范化的所有数据)。 但是在更新数据时我们会遇到其他问题。 也许这就是讨论的核心?

来自多年的面向对象开发...... Redux 感觉像是倒退了一大步。 我发现我自己乞求创建封装事件(动作创建者)和业务逻辑的类。 我仍在试图找出一个有意义的妥协,但到目前为止还没有这样做。 有没有其他人有同样的感觉?

面向对象的编程鼓励将读取和写入放在一起。 这会导致一系列问题:快照和回滚、集中日志记录、调试错误的状态突变、粒度有效更新。 如果您不觉得这些是您的问题,如果您在编写传统的面向对象 MVC 代码时知道如何避免它们,并且如果 Redux 引入的问题多于它在您的应用程序中解决的问题,请不要使用 Redux :wink: .

@jayesbe来自面向对象的编程背景,您可能会发现它与编程中的新兴思想相冲突。 这是关于该主题的众多文章之一: https :

通过将操作与数据转换分离,业务规则应用的测试更加简单。 转换变得不那么依赖于应用程序上下文。

这并不意味着放弃 Javascript 中的对象或类。 例如,React 组件被实现为对象,现在是类。 但是 React 组件的设计只是为了创建所提供数据的投影。 鼓励您使用不存储状态的纯组件。

这就是 Redux 的用武之地:组织应用程序状态并将操作和应用程序状态的相应转换组合在一起。

@johnsoftek感谢您的链接。 然而,根据我过去 10 年的经验......我不同意它,但我们不需要在这里讨论 OO 和非 OO 之间的争论。 我的问题是组织代码和抽象。

我的目标是创建一个单一的应用程序/单一架构,可以(单独使用配置值)用于创建 100 个应用程序。 我必须处理的用例是处理许多客户正在使用的白标软件解决方案。每个客户都调用自己的应用程序。

我提出了一个我认为有趣的折衷方案。我认为它可以很好地处理它,但它可能不符合函数式编程人群的标准。 我还是想把它放在那里。

我有一个 Application 类,它包含所有业务逻辑、API 包装器等,我需要与服务器端应用程序进行交互。

例子..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

我创建了这个对象的一个​​实例并将它放入上下文中..通过扩展 Redux Provider

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

然后我使用了这个提供者

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

最后在我的组件中我使用了我的应用程序..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

因此,Action Creator 只是我的业务逻辑的回调函数。

                      app.login(value, actions.login);

尽管我刚刚开始进行身份验证,但该解决方案目前似乎运行良好。

我相信我也可以将商店传递到我的 Application 实例中,但我不想这样做,因为我不希望商店经历机会突变。 虽然访问商店可能会派上用场。 如果需要,我会考虑更多。

我提出了一个我认为有趣的折衷方案。我认为它可以很好地处理它,但它可能不符合函数式编程人群的标准。

你不会在这里找到“功能性人群” :wink: 。 我们在 Redux 中选择函数式解决方案的原因不是因为我们教条,而是因为它们解决了人们经常因为类而产生的一些问题。 例如,将reducer 与action creators 分开可以让我们分离读取和写入,这对于记录和重现错误很重要。 作为普通对象的动作使记录和重放成为可能,因为它们是可序列化的。 类似地,状态是普通对象而不是MyAppState的实例,这使得在服务器上对其进行序列化并在客户端反序列化以供服务器渲染或将其部分保留在localStorage变得非常容易。 将reducer 表示为函数允许我们实现时间旅行和热重载,将选择器表示为函数可以轻松添加记忆。 所有这些好处都与我们成为“功能性人群”无关,而与解决这个库旨在解决的特定任务有关。

我创建了这个对象的一个​​实例并将它放入上下文中..通过扩展 Redux Provider

这对我来说看起来很明智。 我们对课堂没有非理性的仇恨。 关键是我们宁愿在它们受到严格限制的情况下(例如对于减速器或动作对象)不使用它们,但是使用它们来生成动作对象也很好,例如。

然而,我会避免扩展Provider因为这很脆弱。 不需要它:React 合并组件的上下文,因此您可以将其包装起来。

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

在我看来,它实际上读起来更容易,而且不那么脆弱。

所以,总而言之,你的方法完全有道理。 在这种情况下使用类与createActions(config)类的东西并没有真正不同,如果您需要参数化动作创建者,我们也推荐这种模式。 这绝对没有问题。

我们只是不鼓励您将类实例用于状态和操作对象,因为类实例使序列化非常棘手。 对于reducer,我们也不推荐使用类,因为使用reducer组合会比较困难,即调用其他reducer的reducer。 对于其他一切,您可以使用任何代码组织方式,包括类。

如果您的应用程序和配置是不可变的(我认为它们应该是不可变的,但也许我喝了太多功能性的cool-aid),那么您可以考虑以下方法:

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

然后在 mapStateToProps 中:

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

然后你不需要你采用的提供者技术,你只需从状态中获取应用程序对象。 由于重新选择,Application 对象只会在配置更改时构造,这可能只是一次。

我认为这种方法可能有一个优点,它可以轻松地让您将想法扩展到拥有多个这样的对象,并且让这些对象依赖于状态的其他部分。 例如,您可以拥有一个带有登录/注销/等方法的 UserControl 类,该类可以访问配置和部分状态。

所以,总而言之,你的方法完全有道理。

:+1:谢谢,这有帮助。 我同意 MyProvider 的改进。 我会更新我的代码以遵循。 我第一次学习 Redux 时遇到的最大问题之一是“Action Creators”的语义概念……直到我将它们与事件等同起来时,它才开始动摇。 对我来说,这是一种认识,就像……这些是正在发送的事件。

@winstonewert在 react-native 上可以使用 createSelector 吗? 我不相信是这样。 同时,每次将它附加到某个组件的 mapStateToProps 中时,它看起来好像都在创建新应用程序? 我的目标是实例化单个对象,为应用程序提供所有业务逻辑,并使该对象可全局访问。 我不确定你的建议是否有效。 虽然我喜欢在需要时提供额外对象的想法......从技术上讲,我也可以根据需要通过 Application 实例进行实例化。

我第一次学习 Redux 时遇到的最大问题之一是“Action Creators”的语义概念……直到我将它们与事件等同起来时,它才开始动摇。 对我来说,这是一种认识,就像……这些是正在发送的事件。

我会说 Redux 中根本没有 Action Creators 的语义概念。 Actions 有一个语义概念(它描述了发生的事情,大致相当于事件,但不完全相同——例如参见#351 中的讨论)。 动作创建者只是组织代码的一种模式。 拥有动作工厂很方便,因为你想确保相同类型的动作具有一致的结构,在分派之前具有相同的副作用等。但是从 Redux 的角度来看,动作创建者并不存在——Redux只看到行动。

在 react-native 上可以使用 createSelector 吗?

它在Reselect中可用,它是没有依赖关系的纯 JavaScript,可以在 Web、本机、服务器等上工作。

啊。 好的,我知道了。 事情就清楚多了。 干杯。

不要在 mapStateToProps 和 mapDispatchToProps 中嵌套对象

我最近遇到了一个问题,当 Redux 合并 mapStateToProps 和 mapDispatchToProps 时嵌套对象丢失(参见 reactjs/react-redux#324)。 尽管@gaearon提供了一个允许我使用嵌套对象的解决方案,但他继续说这是一种反模式:

请注意,像这样对对象进行分组会导致不必要的分配,也会使性能优化变得更加困难,因为我们不能再依赖结果道具的浅等性来判断它们是否发生了变化。 因此,与我们在文档中推荐的没有命名空间的简单方法相比,您将看到更多的渲染。

@bvaughn

减速器应该是愚蠢而简单的

我们应该将大多数业务逻辑放入动作创建者中,我完全同意这一点。 但是如果一切都变成了行动,为什么我们仍然需要手动创建 reducer 文件和函数? 为什么不直接把action中操作过的数据放到store中呢?

这让我困惑了一段时间......

为什么我们仍然需要手动创建 reducer 文件和函数?

因为reducer是纯函数,如果状态更新逻辑有错误,可以热重载reducer。 然后,开发工具可以将应用程序回退到其初始状态,然后使用新的减速器重放所有操作。 这意味着您可以修复状态更新错误,而无需手动回滚并重新执行操作。 这是将大部分状态更新逻辑保留在减速器中的好处。

这是将大部分状态更新逻辑保留在减速器中的好处。

@dtinth只是澄清

我不太确定将您的大部分逻辑放在动作创建者中是个好主意。 如果您所有的 reducer 都只是接受ADD_X类的操作的简单函数,那么它们就不太可能有错误 - 太好了! 但是随后您的所有错误都被推送给了您的动作创建者,您将失去@dtinth 所暗示的出色调试体验。

但也像@tommikaikkonen提到的那样,编写复杂的减速器并不是那么简单。 我的直觉是,如果您想获得 Redux 的好处,那么这就是您想要推动的地方 - 否则不是将副作用推到边缘,而是推动您的纯函数只处理最琐碎的任务,留下大多数您的应用程序处于状态混乱的地狱中。 :)

@sompylasar “业务逻辑”和“状态更新逻辑”,imo,是一回事。

然而,为了配合我自己的实现细节......我的动作主要是对动作输入的查找。 实际上,我所有的操作都是纯粹的,因为我已将所有“业务逻辑”移动到应用程序上下文中。

举个例子..这是我典型的减速器

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

我的典型动作:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

我的业务逻辑是在存储在上下文中的对象中定义的,即..

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

如果你看到我上面的评论,我也在努力寻找最好的方法..我最终重构并解决了将存储传递到我的应用程序对象的构造函数并将所有操作连接到这个中心点的调度程序的问题。 我的应用程序知道的所有操作都在此处分配。

我不再在任何地方使用 mapDispatchToProps()。 对于 Redux,我现在只在创建连接组件时使用 mapStateToProps。 如果我需要触发任何操作..我可以通过上下文通过我的应用程序对象触发它们。

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

上述组件不需要 redux 连接。 它仍然可以调度动作。 当然,如果您需要更新组件内的状态,您可以将其转换为连接的组件,以确保传播状态更改。

这就是我组织核心“业务逻辑”的方式。 由于我的状态实际上是在后端服务器上维护的。这对我的用例非常有效。

您存储“业务逻辑”的位置完全取决于您以及它如何适合您的用例。

@jayesbe以下部分意味着您在减速器中没有“业务逻辑”,而且,状态结构已转移到动作创建器中,这些动作创建器创建了通过减速器传输到存储中的有效负载:

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

@jayesbe我的actions和reducers和你的很相似,一些actions接收一个普通的网络响应对象作为参数,我将如何处理响应数据的逻辑封装到action中,最后返回一个非常简单的对象作为返回值然后传递给reducer 通过调用 dispatch()。 就像你所做的那样。 问题是如果你的action写成这样,你的action几乎已经做了所有的事情,你的reducer的责任会很轻,如果reducer只是简单的传播action对象,为什么我们还要手动传输数据来存储呢? Redux 自动为我们提供剂量根本不是一件困难的事情。

不必要。 但很多时候,业务流程的一部分
涉及根据业务规则更新应用程序的状态,
所以你可能需要在那里放一些业务逻辑。

对于极端情况,请查看:
“同步 RTS 引擎和不同步的故事”@ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

2016 年 4 月 5 日下午 5:54,“John Babak”通知@ github.com 写道:

这是将大多数状态更新逻辑保留在
减速器。

@dtinth https://github.com/dtinth只是为了澄清,说
“状态更新逻辑”是指“业务逻辑”吗?


你收到这个是因为你被提到了。
直接回复此邮件或在 GitHub 上查看
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

@LumiaSaki在动作创建者中保持复杂逻辑的同时保持减速器简单的建议违背了使用 Redux 的推荐方法。 Redux 推荐的模式正好相反:保持动作创建者简单,同时在减速器中保持复杂逻辑。 当然,您无论如何都可以自由地将所有逻辑放在动作创建器中,但是这样做并没有遵循 Redux 范式,即使您正在使用 Redux。

因此,Redux 不会自动将数据从操作传输到存储。 因为这不是您应该使用 Redux 的方式。 Redux 不会被更改以促进以不同于预期的方式使用它。 当然,你完全可以自由地做适合你的事情,但不要指望 Redux 会为此而改变。

就其价值而言,我使用以下方法生产减速器:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

defineActions 返回减速器和动作创建者。 通过这种方式,我发现将更新逻辑保留在 reducer 中非常容易,而不必花费大量时间编写琐碎的动作创建者。

如果您坚持将您的逻辑保留在您的动作创建器中,那么您自己自动化数据应该不会有任何问题。 你的 reducer 是一个函数,它可以做任何它想做的事。 所以你的减速机可以简单到:

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

扩展前面关于业务逻辑的要点,我认为您可以将业务逻辑分为两部分:

  • 非确定性部分。 这部分使用外部服务、异步代码、系统时间或随机数生成器。 在我看来,这部分最好由 I/O 函数(或动作创建者)处理。 我称它们为业务流程。

考虑一个类似 IDE 的软件,用户可以在其中单击运行按钮,它会编译并运行应用程序。 (这里我使用了一个带有 store 的 async 函数,但你可以使用redux-thunk代替。)

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

  • 确定性部分。 这部分具有完全可预测的结果。 给定相同的状态和相同的事件,结果总是可以预测的。 在我看来,这部分最好由减速器处理。 我称之为业务规则。

``` js
从 'updeep' 导入你

export const reducer = createReducer({
// [动作名称]:动作 => currentState => nextState
开始编译:() => u({编译:真}),
errorCompiling: ({ error }) => u({编译: false, compileError: error }),
startRunning: ({ app }) => u({
运行:() => 应用程序,
编译:假
}),
stopRunning: () => u({ running: false }),
丢弃编译错误: () => u({ compileError: null }),
// ...
})
``

我尝试将尽可能多的代码放入这个确定性的领域,同时记住,给定传入的操作,reducer 的唯一责任是保持应用程序状态一致。 而已。 除此之外,我会在 Redux 之外进行,因为 Redux 只是一个状态容器。

@dtinth太好了,因为https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 中的前一个示例看起来与您在https://github.com/reactjs/redux/ 中编写的完全不同

@winstonewert

Redux 推荐的模式正好相反:保持动作创建者简单,同时在减速器中保持复杂逻辑。

你怎么能把复杂的逻辑放在 ruducers 中,并且仍然保持它们的纯净?

例如,如果我正在调用 fetch() 并从服务器加载数据......然后以某种方式处理它。 我还没有看到一个具有“复杂逻辑”的减速器的例子

@jayesbe :呃……“复杂”和“纯”是正交的。 您可以在 reducer 中包含 _really_ 复杂的条件逻辑或操作,只要它只是其输入的函数,没有副作用,它仍然是纯的。

如果您有复杂的本地状态(想想帖子编辑器、树视图等)或处理乐观更新之类的事情,您的减速器将包含复杂的逻辑。 这真的取决于应用程序。 有些有复杂的请求,有些有复杂的状态更新。 有些人两者都有:-)

@markerikson ok 逻辑语句是一回事……但是执行特定任务? 就像说我有一个动作,在一种情况下触发了其他三个动作,或者在另一种情况下触发了两个不同且独立的动作。 任务的逻辑 + 执行听起来不应该放在减速器中。

我的状态数据/模型状态在服务器上,视图状态与数据模型不同,但该状态的管理在客户端。 我的数据模型状态被简单地传递到视图中......这就是我的减速器和动作如此精简的原因。

@jayesbe :我认为没有人说过其他动作的触发应该放在减速器中。 事实上,它不应该。 减速器的工作只是(currentState + action) -> newState

如果您需要将多个动作联系在一起,您可以使用类似 thunk 或 saga 之类的方式执行并按顺序触发它们,或者使用一些监听状态变化的东西,或者使用中间件来拦截一个动作并做额外的工作。

老实说,我对此时讨论的内容有些困惑。

@markerikson这个话题似乎是关于“业务逻辑”及其

正如您所指出的,真正重要的是对您有用的东西。 但这是我将如何解决您提出的问题。

例如,如果我正在调用 fetch() 并从服务器加载数据......然后以某种方式处理它。 我还没有看到一个具有“复杂逻辑”的减速器的例子

我的减速器从我的服务器获取原始响应并用它更新我的状态。 这样你所说的处理响应是在我的减速器中完成的。 例如,请求可能是为我的服务器获取 JSON 记录,reducer 将其保存在我的本地记录缓存中。

k 个逻辑语句是一回事……但是执行特定任务? 就像说我有一个动作,在一种情况下触发了其他三个动作,或者在另一种情况下触发了两个不同且独立的动作。 任务的逻辑 + 执行听起来不应该放在减速器中。

这取决于你在做什么。 显然,在服务器获取的情况下,一个动作会触发另一个动作。 这完全在推荐的 Redux 程序之内。 但是,您也可能正在执行以下操作:

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

这不是使用 Redux 的推荐方式。 推荐的 Redux 方法是使用单个 CREATE_FOOBAR 操作来导致所有这些所需的更改。

@温斯通沃特

这不是使用 Redux 的推荐方式。 推荐的 Redux 方法是使用单个 CREATE_FOOBAR 操作来导致所有这些所需的更改。

你有一个指向指定地方的指针吗? 因为当我为 FAQ 页面做研究时,我想到的是“这取决于”,直接来自 Dan。 请参阅http://redux.js.org/docs/FAQ.html#actions -multiple-actions 和Dan 在 SO 上的回答

“业务逻辑”确实是一个相当宽泛的术语。 它可以涵盖诸如“发生了什么事?”、“这件事发生后我们该怎么办?”、“这是否有效?”等等。 基于 Redux 的设计,这些问题_可以_根据情况在不同的地方回答,尽管我将“是否发生了”更多地视为动作创建者的责任,而“现在发生了什么”几乎肯定是减速器的责任。

总的来说,我对“业务逻辑”的_整个问题_“这取决于_”。 您可能希望在动作创建者中进行请求解析的原因,以及您可能希望在减速器中进行解析的原因。 有时您的减速器可能只是“将这个对象放入我的状态”,而有时您的减速器可能是非常复杂的条件逻辑。 有时您的动作创建者可能非常简单,有时可能很复杂。 有时,连续调度多个动作来表示流程的步骤是有意义的,而有时您只想调度一个通用的“THING_HAPPENED”动作来表示所有步骤。

关于我同意的唯一硬性规则是“动作创建者中的非确定性,reducer 中的纯粹确定性”。 那是给定的。

除此之外? 找到适合你的东西。 始终如一。 知道你为什么以某种方式这样做。 随它去。

尽管我将“是否发生过”更多地视为动作创建者的责任,而“现在发生了什么”几乎肯定是减速器的责任。

这就是为什么有一个平行的讨论如何将副作用,即“现在做什么”的非纯部分放入减速器:#1528 并使它们只是应该发生的事情的纯描述,比如接下来要调度的动作。

我一直在使用的模式是:

  • 做什么:动作/动作创建者
  • 如何做:中间件(例如,侦听“异步”操作并调用我的 API 对象的中间件。)
  • 如何处理结果:Reducer

在本主题的前面,丹的声明是:

所以我们官方的建议是,你应该首先尝试让不同的减速器响应相同的动作。 如果它变得尴尬,那么当然,制作单独的动作创建者。 但不要从这种方法开始。

因此,我认为推荐的方法是为每个事件分派一个动作。 但是,务实地,做有效的事情。

@winstonewert :Dan 指的是“reducer 组合”模式,即“一个动作只由一个减速器听过”与“许多减速器可以响应相同的动作”。 Dan 非常擅长响应单个动作的任意减速器。 其他人更喜欢“鸭子”方法,其中减速器和动作非常紧密地捆绑在一起,并且只有一个减速器处理给定的动作。 所以,这个例子不是关于“按顺序调度多个动作”,而是“我的减速器结构的多少部分期望对此做出响应”。

但是,务实地,做有效的事情。

:+1:

@sompylasar通过在我的操作中使用状态结构,我看到了我的方式的错误。 我可以轻松地将状态结构转移到我的减速器中并简化我的动作。 干杯。

在我看来,它是一样的。

要么您有一个操作触发多个减速器导致多个状态更改,要么您有多个操作每个触发一个减速器导致单个状态更改。 让多个 reducer 响应一个动作,让一个事件分派多个动作是同一问题的替代解决方案。

在你提到的 StackOverflow 问题中,他说:

使操作日志尽可能接近用户交互的历史记录。 但是,如果它使 reducer 难以实现,请考虑将某些操作拆分为多个操作,如果 UI 更新可以被认为是两个碰巧在一起的单独操作。

在我看来,Dan 赞同将每个用户交互保持一个操作作为理想的方式。 但是他很务实,当它使减速器难以实现时,他支持拆分动作。

我在这里可视化了几个相似但有些不同的用例:

1) 一个动作需要更新你状态的多个区域,特别是如果你使用combineReducers来让单独的 reducer 函数处理每个子域。 你:

  • 让 Reducer A 和 Reducer B 响应相同的操作并独立更新它们的状态位
  • 让你的 thunk 把所有相关的数据都放到 action 中,这样任何 reducer 都可以访问它自己的块之外的其他状态位
  • 添加另一个顶级减速器来获取减速器 A 的状态,并将它的特殊情况交给减速器 B?
  • 为 Reducer A 发送一个带有它需要的状态位的操作,以及为 Reducer B 发送它需要什么的第二个操作?

2) 您有一组按特定顺序发生的步骤,每个步骤都需要一些状态更新或中间件操作。 你:

  • 将每个单独的 step 作为单独的 action 来表示进度,并让单独的 reducer 响应特定的 action?
  • 调度一个大事件,并相信减速器会适当地处理它?

所以是的,肯定有一些重叠,但我认为这里心理图的部分差异是各种用例。

@markerikson所以你的建议是'这取决于你遇到什么情况',如何平衡动作或reducer的'业务逻辑'只是你的考虑,我们也应该尽可能多地利用纯函数的好处?

是的。 Reducers _have_ 是纯的,作为 Redux 的要求(除了 0.00001% 的特殊情况)。 动作创造者绝对_不必_必须是纯洁的,事实上,你的大部分“杂质”都会存在。 然而,由于纯函数显然比不纯函数更容易理解和测试,_if_你可以让你的一些动作创建逻辑变得纯粹,太棒了! 如果没有,那很好。

是的,从我的角度来看,作为开发人员,您应该为自己的应用程序逻辑及其所在位置确定适当的平衡点。 对于动作创建者/化简器划分的哪一边它应该存在,没有单一的硬性规则。 (呃,除了我上面提到的“确定性/非确定性”的东西。我在这篇评论中显然是要引用的。显然。)

@cpsubrian

如何处理结果:Reducer

实际上这就是为什么传奇是为了:处理诸如“如果发生了,那也应该发生”之类的效果


@markerikson @LumiaSaki

动作创造者绝对不必是纯洁的,事实上,你的大部分“杂质”都会存在。

实际上,动作创造者甚至不需要是不纯洁的,甚至不需要存在。
http://stackoverflow.com/a/34623840/82609

是的,从我的角度来看,作为开发人员,您应该为自己的应用程序逻辑及其所在位置确定适当的平衡点。 对于动作创建者/化简器划分的哪一边它应该存在,没有单一的硬性规则。

是的,但在没有经验的情况下注意到每种方法的缺点并不那么明显:) 另请参阅我的评论: https :

没有严格的规则适用于大多数简单的应用程序,但如果你想构建可重用的组件,这些组件不应该知道它们自己范围之外的东西。

因此,与其为整个应用程序定义全局操作列表,您可以开始将应用程序拆分为可重用的组件,每个组件都有自己的操作列表,并且只能分派/减少这些操作。 那么问题是,你如何表达“当在我的日期选择器中选择日期时,我们应该在该待办事项上保存一个提醒,显示一个反馈吐司,然后将应用程序导航到带有提醒的待办事项”:这是传奇开始行动:编排组件

另见https://github.com/slorber/scalable-frontend-with-elm-or-redux

是的,从我的角度来看,作为开发人员,您应该为自己的应用程序逻辑及其所在位置确定适当的平衡点。 对于动作创建者/化简器划分的哪一边它应该存在,没有单一的硬性规则。

是的,Redux 没有要求您将逻辑放在减速器中还是动作创建器中。 Redux 不会以任何方式中断。 没有硬性规定要求您以一种或另一种方式来做。 但 Dan 的建议是“让操作日志尽可能接近用户交互的历史记录”。 不需要为每个用户事件分派单个操作,但建议这样做。

就我而言,我有 2 个对 1 个动作感兴趣的减速器。 原始 action.data 是不够的。 他们需要处理转换后的数据。 我不想在 2 个减速器中执行转换。 所以我移动了函数来执行转换为 thunk。 这样我的减速器就会收到一个准备好消费的数据。 这是我在短短 1 个月的 redux 体验中所能想到的最好的结果。

如何将组件/视图与商店的结构解耦? 我的目标是任何受商店结构影响的东西都应该在减速器中进行管理,这就是为什么我喜欢将选择器与减速器并置,因此组件并不真正需要知道如何获取商店的特定节点。

这对于将数据传递给组件非常有用,反之,当组件分派动作时:

例如,在 Todo 应用程序中,我正在更新 Todo 项目的名称,因此我分派了一个传递我想要更新的项目部分的操作,即:

dispatch(updateItem({name: <text variable>}));

,动作定义为:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

这又由减速器处理,它可以简单地执行以下操作:

Object.assign({}, item, action.updatedData)

更新项目。

这很有效,因为我可以重用相同的操作和减速器来更新 Todo 项目的任何道具,即:

updateItem({description: <text variable>})

当描述被更改时。

但是这里的组件需要知道一个 Todo 项目是如何在商店中定义的,如果该定义发生变化,我需要记住在依赖它的所有组件中更改它,这显然是一个坏主意,对这种情况有什么建议吗?

@dcoellarb

我在这种情况下的解决方案是利用 Javascript 的灵活性来生成样板文件。

所以我可能有:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

其中 makeRecord 是一个根据我的描述自动构建减速器、动作创建器和选择器的函数。 这消除了样板文件,但是如果我以后需要做一些不适合这种整洁模式的事情,我可以将自定义减速器/操作/选择器添加到 makeRecord 的结果中。

tks @winstonewert我喜欢避免样板的方法,我可以看到在具有大量模型的应用程序中节省了大量时间; 但我仍然没有看到这将如何将组件与存储结构解耦,我的意思是即使生成了动作,组件仍然需要将更新的字段传递给它,这意味着组件仍然需要知道的结构商店对吗?

@winstonewert @dcoellarb在我看来,动作负载结构应该属于动作,而不是减速器,并在减速器中显式转换为状态结构。 为了最初的简单性,这些结构相互反映应该是一个幸运的巧合。 这两个结构不需要总是互相镜像,因为它们不是同一个实体。

@sompylasar是的,我这样做了,我将 api/rest 数据转换为我的存储结构,但仍然唯一应该知道存储结构的是减速器和选择器,对吗? 我之所以将它们放在一起,我的问题在于组件/视图,我希望它们不需要知道存储结构,以防我决定稍后更改它,但正如我的示例中所述,它们需要知道结构,以便他们可以发送要更新的正确数据,我还没有找到更好的方法:(。

@dcoellarb您可能会将您的视图视为某种类型数据的输入(如字符串或数字,但具有字段的结构化对象)。 该数据对象不一定反映存储结构。 您将此数据对象放入操作中。 这不是与商店结构耦合。 存储和视图都必须与动作负载结构耦合。

@sompylasar 很有道理,我会试试的,非常感谢!!!

我可能还应该补充一点,您可以通过使用redux-saga使操作更加纯粹。 然而,redux-saga 难以处理异步事件,因此您可以通过使用 RxJS(或任何 FRP 库)而不是 redux-saga 将这个想法更进一步。 这是使用 KefirJS 的示例: https :

@frankandrobot

redux-saga 努力处理异步事件

你这是什么意思? redux-saga不是以优雅的方式处理异步事件和副作用吗? 看看https://github.com/reactjs/redux/issues/1171#issuecomment -167715393

没有@IceOnFire 。 上次我阅读redux-saga文档时,处理复杂的异步工作流很困难。 参见,例如:http: //yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
它说(仍然说?)大意

我们将把其余的细节留给读者,因为它开始变得复杂

将其与 FRP 方式进行比较: https :
整个工作流程都得到了完全处理。 (我可能只添加了几行。)除此之外,您仍然可以获得redux-saga大部分优点(一切都是纯函数,主要是简单的单元测试)。

上次我想到这个时,我得出的结论是,问题是 redux-saga 使一切看起来都是同步的。 它非常适合简单的工作流程,但对于复杂的异步工作流程,如果您明确处理异步工作会更容易……这就是 FRP 的优势所在。

@frankandrobot

谢谢你的解释。 我没有看到你提到的引用,也许是图书馆进化了(例如,我现在看到了我以前从未见过的cancel效果)。

如果这两个示例(saga 和 FRP)的行为完全相同,那么我看不出有太大区别:一个是 try/catch 块内的指令序列,而另一个是流上的一系列方法。 由于我在流方面缺乏经验,我什至发现 saga 示例更具可读性,并且更具可测试性,因为您可以逐个测试每个yield 。 但我很确定这更多是由于我的心态而不是技术。

无论如何,我很想知道@yelouafi 对此的看法。

@bvaughn你能在你在这里描述的同一个测试中指出任何像样的测试动作、减速器、选择器的例子吗?

测试动作、reducer 和选择器的最有效方法是在编写测​​试时遵循“鸭子”方法。 这意味着您应该编写一组测试来涵盖一组给定的操作、reducer 和选择器,而不是 3 组单独关注每个的测试。 这更准确地模拟了实际应用程序中发生的情况,并提供了最大的收益。

@morgs32 😄

这个问题有点老了,我有一段时间没有使用过 Redux。 Redux 站点上有一个关于编写测试的部分,您可能想查看一下。

基本上,我只是指出 - 而不是单独为动作和减速器编写测试 - 将它们一起编写会更有效,如下所示:

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

这只是我在一个项目上使用 Redux 几个月后的观察。 这不是官方建议。 天啊。 👍

“在 action-creators 中做更多,在 reducer 中做更少”

如果应用程序是服务器和客户端并且服务器必须包含业务逻辑和验证器怎么办?
所以我按原样发送动作,大部分工作将由减速器在服务器端完成......

首先,对不起我的英语。 但我有一些不同的意见。

我的选择是fat reducer, thin action creators。

我的动作创建者只是基于一些 __promise 中间件__ 的 __dispatch__ 动作(async、sync、serial-async、parallel-async、parallel-async in for-loop)。

我的reducer 分成许多小的状态片来处理业务逻辑。 使用combineReduers组合它们。 reducer是__pure function__,所以很容易被重用。 也许有一天我会使用 angularJS,我想我可以在我的服务中重新使用reducer来实现相同的业务逻辑。 如果你的reducer有很多行代码,它可能可以拆分成更小的 reducer 或者抽象一些函数。

是的,有一些跨状态的情况,这意味着A取决于B, C .. 和B, C是异步数据。 我们必须使用B,C来填充或初始化 A。这就是我使用crossSliceReducer

关于__在动作创建者中做得更多,在减速器中做得更少__。

  1. 如果你使用redux-thunk或等等。是的。 您可以通过getState()访问动作创建者中的完整状态。 这是一个选择。 或者,您可以创建一些 __crossSliceReducer__,这样您也可以访问完整状态,您可以使用一些状态切片来计算您的其他状态。

关于__单元测试__

reducer是__纯函数__。 所以很容易被测试。 现在,我只是测试我的减速器,因为它比其他部分最重要。

测试action creators ? 我认为如果他们“胖”,可能不容易进行测试。 尤其是 __async 动作创建者__。

我同意你@mrdulin ,这也是我现在的做法。

@mrdulin是的。 看起来中间件是放置不纯逻辑的正确位置。
但是对于业务逻辑,reducer 似乎不是一个合适的地方。 您最终会得到多个“综合”操作,这些操作不代表用户的要求,而是您的业务逻辑要求。

更简单的选择是从中间件调用一些纯函数/类方法:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

@bvaughn

这减少了导致细微未定义值的错误输入变量的可能性,它简化了对商店结构的更改等。

我反对在任何地方使用选择器的情况,即使对于不需要记忆或任何类型的数据转换的微不足道的状态:

  • 减速器的单元测试不应该已经捕获错误类型的状态属性吗?
  • 根据我的经验,商店结构的变化并不经常发生,主要是在项目的启动阶段。 稍后,如果您将state.stuff.item1更改state.stuff.item2您可以搜索代码并在任何地方更改它 - 就像更改其他任何东西的名称一样。 对于使用 IDE 的人来说,这是一项常见的任务,而且无需动脑。
  • 在任何地方使用选择器是一种空洞的抽象。 简单状态的这种担忧。 当然,您可以通过使用此 API 访问状态来获得一致性,但您放弃了简单性。

显然选择器是必要的,但我想听听其他一些使它们成为强制性 API 的论据。

从实践的角度来看,根据我的经验,一个很好的理由是 reselect 或 redux-saga 等库利用选择器来访问状态片段。 这足以让我坚持使用选择器。

从哲学上讲,我总是将选择器视为功能世界的“getter 方法”。 出于同样的原因,我永远不会访问 Java 对象的公共属性,我永远不会直接在 Redux 应用程序中访问子状态。

@IceOnFire如果计算不昂贵或不需要数据转换,则没有什么可利用的。

Getter 方法可能是 Java 中的常见做法,但直接在 JS 中访问 POJO 也是如此。

@timotgl

为什么在商店和其他 redux 代码之间有一个 API?

选择器是减速器的查询(读取)公共 API,动作是减速器的命令(写入)公共 API。 Reducer 的结构就是它的实现细节。

选择器和动作在 UI 层和 saga 层(如果您使用 redux-saga)中使用,而不是在 reducer 本身中使用。

@sompylasar不确定我在这里是否遵循您的观点。 除了动作之外别无选择,我必须使用它们来与 redux 进行交互。 我没有然而,使用选择,我可以随便挑东西直接从当它被暴露的状态,它是由设计。

您正在描述一种将选择器视为减速器的“读取”API 的方法,但我的问题是首先将选择器作为强制性 API 的理由是什么(强制作为项目中的最佳实践,而不是通过图书馆设计)。

@timotgl是的,选择器不是强制性的。 但是它们是一个很好的实践,可以为应用程序代码中的未来更改做好准备,从而可以单独重构它们:

  • 如何构造和写入某些状态(reducer 关注点,一处)
  • 如何使用那块状态(UI 和副作用问题,从许多地方查询同一块状态)

当您将要更改存储结构时,如果没有选择器,您必须查找并重构访问受影响状态部分的所有位置,这可能是一项非常重要的任务,而不仅仅是查找和-替换,特别是如果您传递直接从商店获得的状态片段,而不是通过选择器。

@sompylasar感谢您的投入。

这可能是一项重要的任务

这是一个合理的担忧,对我来说这似乎是一个昂贵的权衡。 我想我还没有遇到过导致此类问题的状态重构。 然而,我遇到过“选择器意大利面”,其中每个微不足道的状态子部分的嵌套选择器引起了相当大的混乱。 毕竟,这种对策本身也必须保持。 但我现在更好地理解了这背后的原因。

@timotgl一个我可以公开分享的简单例子:

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

存储的内容是state, valueOrError还是其他内容,并不是这个 reducer 用户关心的问题。 公开的是状态字符串(枚举),几个经常使用的状态检查,值和错误。

然而,我遇到过“选择器意大利面”,其中每个微不足道的状态子部分的嵌套选择器引起了相当大的混乱。

如果这个嵌套是由镜像减速器嵌套(组合)引起的,那不是我推荐的,那就是减速器的实现细节。 使用上面的示例,对其状态的某些部分使用 promiseReducer 的 reducer 导出根据这些部分命名的自己的选择器。 此外,并非每个看起来像选择器的函数都必须导出并成为 reducer API 的一部分。

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

哦,还有一件事我差点忘了。 函数名称和导出/导入可以很好且安全地缩小。 嵌套对象键——不是那么多,你需要在压缩器中使用适当的数据流跟踪器,以免搞砸代码。

@timotgl :我们鼓励的许多 Redux 最佳实践都是关于尝试封装与 Redux 相关的逻辑和行为。

例如,您可以通过执行this.props.dispatch({type : "INCREMENT"})直接从连接的组件分派动作。 但是,我们不鼓励这样做,因为它会强制组件“知道”它正在与 Redux 对话。 更符合 React 习惯的做事方式是传入绑定的动作创建者,这样组件就可以调用this.props.increment() ,并且该函数是否是绑定的 Redux 动作创建者并不重要,传递的回调被父母或测试中的模拟函数击倒。

您也可以在任何地方手写操作类型,但我们鼓励定义常量变量,以便可以导入、跟踪它们并减少拼写错误的机会。

同样,没有什么可以阻止您在mapState函数或 thunk 中访问state.some.deeply.nested.field 。 但是,正如本线程中已经描述的那样,这增​​加了拼写错误的机会,使追踪特定状态的位置变得更加困难,使重构更加困难,并且意味着任何昂贵的转换逻辑都可能是重新- 每次都运行,即使它不需要。

所以不,您不必_必须_使用选择器,但它们是一种很好的架构实践。

您可能想通读我的文章Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance

@markerikson我不反对一般的选择器,也不反对昂贵计算的选择器。 而且我从未打算将 dispatch 本身传递给组件:)

我的观点是我不同意这种信念:

理想情况下,只有您的 reducer 函数和选择器应该知道确切的状态结构,因此如果您更改某些状态所在的位置,您只需要更新这两个逻辑。

关于你的例子state.some.deeply.nested.field ,我可以看到有一个选择器来缩短它的价值。 selectSomeDeeplyNestedField()一个函数名称与 5 个我可能会出错的属性。

另一方面,如果你按照这封信的指导方针,你也在做const selectSomeField = state => state.some.field;甚至const selectSomething = state => state.something; ,并且在某些时候持续这样做的开销(导入,导出,测试)在我看来,不再证明(有争议的)安全性和纯度是合理的。 这是善意的,但我无法摆脱指南的教条精神。 我相信我的项目中的开发人员会在适用时明智地使用选择器 - 因为到目前为止我的经验是他们这样做。

在某些情况下,我可以理解为什么你会想要在安全和惯例方面犯错。 感谢您的时间和参与,总的来说我很高兴我们有了 redux。

当然。 FWIW,还有其他选择器库,以及 Reselect 周围的包装器。 例如, https://github.com/planttheidea/selectorator允许您定义点符号键路径,并在内部为您执行中间选择器。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

cloudfroster picture cloudfroster  ·  3评论

timdorr picture timdorr  ·  3评论

captbaritone picture captbaritone  ·  3评论

ilearnio picture ilearnio  ·  3评论

CellOcean picture CellOcean  ·  3评论