Redux: 一个动作与多个reduces相关时,对combinedReducer的关注

创建于 2015-08-21  ·  52评论  ·  资料来源: reduxjs/redux

我正在使用React.js和Flux构建的聊天应用程序。 当我阅读Redux的文档时, combineReducer函数对我来说似乎很奇怪。 例如:

有三个商店,分别为messageStoreunreadStorethreadStore 。 并且有一个名为NEW_MESSAGE 。 所有这三个商店将更新新消息。 在Redux中,商店是化简版,与combineReducer相结合,例如:

message = (state=[], action) ->
  switch action.type
    when NEW_MESSAGE
      state # new state
unread = (state=[], action) ->
  switch action.type
    when NEW_MESSAGE
      state # new state
thread = (state=[], action) ->
  switch action.type
    when NEW_MESSAGE
      state # new state

combineReducer {message, unread, thread}

在我看来,在这种情况下,分解减速器并不能使我的生活更轻松。 但是,使用immutable-js构建的大型商店可能是一个更好的解决方案。 喜欢:

initialState = Immutable.fromJS
  message: []
  unread: []
  thread: []

allStore = (store=initialState, action) ->
  switch action.type
    when NEW_MESSAGE
        initialState
        .updateIn ['message'], (messages) -> messages # updated
        .updateIn ['urnead'], (urneads) -> unreads # updated
        .updateIn ['thread'], (threads) -> threads # updated
question

最有用的评论

对于这样的操作,我必须将其维护在几个化简器(或我的应用增长后的文件中)中。 在我们当前的代码库中,我们已经看到5家商店处理相同的操作,这很难维护。

这是好是坏取决于您的应用程序。 以我的经验,有许多商店(或Redux精简器)处理相同的操作实际上非常方便,因为它可以划分职责,还可以使人们在功能分支上工作而不会与团队其他成员发生冲突。 以我的经验,单独维护无关的突变比一个巨大的突变要容易得多。

但是您是对的,在某些情况下,这种方法不太有效。 我会说它们通常是次优状态模型的症状。 例如,

在某些情况下,一个reduce可能依赖于另一个reduce的数据(例如将消息从未读迁移到消息,甚至将消息作为新的线程从一个消息推广到另一个线程)

是问题的征兆。 如果必须在状态内部移动很多东西,则状态形状可能需要更规范化。 您可以拥有{ messagesById, threadsById, messageIdsByThreadIds }代替{ readMessages, unreadMessages } { messagesById, threadsById, messageIdsByThreadIds } 。 讯息已读? 好的,切换state.messagesById[id].isRead 。 是否想阅读线程的消息? 抢state.threadsById[threadId].messageIds ,然后抢messageIds.map(id => state.messagesById[id])

数据标准化后,您实际上不需要将项目从一个数组复制到另一个数组。 在大多数情况下,您会翻转标志或添加/删除/关联/取消关联ID。

所有52条评论

您能否详细说明为什么您认为combineReducers在这种情况下没有帮助? 实际上,对于类似数据库的状态,您通常需要一个简化器(然后是UI状态的其他简化器,例如,选定的项目等)。

也可以看看:

https://github.com/rackt/redux/tree/master/examples/real-world/reducers
https://github.com/rackt/redux/blob/master/examples/async/reducers

寻求灵感。

您所指的第一个示例与我的情况类似,两个减速器均处理requestType
https://github.com/rackt/redux/blob/master/examples/real-world/reducers/paginate.js#L28

当两个异径管彼此分离时,分离异径管使其易于维护。 但是,当他们采取相同的行动时,会导致:

  • 对于这样的操作,我必须将其维护在几个化简器(或我的应用增长后的文件中)中。 在我们当前的代码库中,我们已经看到5家商店处理相同的操作,这很难维护。
  • 在某些情况下,一个reduce可能依赖于另一个reduce的数据(例如,将消息从unread移至message ,甚至将消息作为新线程从message移至thread )。 因此,我现在可能需要从另一个减速器中获取数据。

对于这两个问题,我看不到任何好的解决方案。 实际上,我不确定这两个问题,它们就像权衡取舍。

对于这样的操作,我必须将其维护在几个化简器(或我的应用增长后的文件中)中。 在我们当前的代码库中,我们已经看到5家商店处理相同的操作,这很难维护。

这是好是坏取决于您的应用程序。 以我的经验,有许多商店(或Redux精简器)处理相同的操作实际上非常方便,因为它可以划分职责,还可以使人们在功能分支上工作而不会与团队其他成员发生冲突。 以我的经验,单独维护无关的突变比一个巨大的突变要容易得多。

但是您是对的,在某些情况下,这种方法不太有效。 我会说它们通常是次优状态模型的症状。 例如,

在某些情况下,一个reduce可能依赖于另一个reduce的数据(例如将消息从未读迁移到消息,甚至将消息作为新的线程从一个消息推广到另一个线程)

是问题的征兆。 如果必须在状态内部移动很多东西,则状态形状可能需要更规范化。 您可以拥有{ messagesById, threadsById, messageIdsByThreadIds }代替{ readMessages, unreadMessages } { messagesById, threadsById, messageIdsByThreadIds } 。 讯息已读? 好的,切换state.messagesById[id].isRead 。 是否想阅读线程的消息? 抢state.threadsById[threadId].messageIds ,然后抢messageIds.map(id => state.messagesById[id])

数据标准化后,您实际上不需要将项目从一个数组复制到另一个数组。 在大多数情况下,您会翻转标志或添加/删除/关联/取消关联ID。

我想尝试一下。

我刚刚编写了一个演示页面来尝试redux。 我认为combineReducer给我带来了很多复杂性。 回购中的所有示例都使用combineReducer ,是否还有机会我可以使用单个不可变数据对象作为模型,而不是使用包含我的数据的JavaScript对象?

@jiyinyiyong我真的不确定您在说什么复杂性,但是请不要使用combineReducers 。 它只是一个助手。 签出使用Immutable的类似帮助器。 或当然写你自己的。

我编写了自己的combineReducers()辅助程序,主要是为了支持Immutable.js,但它可能需要像根减速器那样处理其他减速器:
https://github.com/jokeyrhyme/wow-healer-bootcamp/blob/master/utils/combineReducers.js

第一个参数匹配状态的形状,并将顶级子状态传递给您提供的匹配子归约器。 这与正式版本大致相同。

但是,可以选择添加零个或多个其他自变量,这些自变量与其他根缩减器一样。 这些减速器通过了完整状态。 这允许以比建议的子归约器模式更多的访问权限的方式调度操作。

显然,这不是一个好主意,但是我发现一个奇怪的情况是,拥有多个子减速器和多个根减速器是一件好事。

也许不是添加多个根减少器,而是中间步骤是使用附加参数来定义需要更广泛访问(仍少于总访问量)的子减少器。 例如:

function a (state, action) { /* only sees within a */ return state; }
function b (state, action) { /* only sees within b */ return state; }
function c (state, action) { /* only sees within c */ return state; }

function ab (state, action) { /* partial state containing a and b */ return state; }
function bc (state, action) { /* partial state containing b and c */ return state; }

let rootReducer = combineReducers(
  { a, b, c },
  [ 'a', 'b', ab ],
  [ 'b', 'c', bc ]
);

是否值得为部分还原剂和其他根系还原剂提交一份PR? 这是一个可怕的想法,还是对其他足够多的人有用?

我更想了解卤素榆木如何解决这些问题。 JavaScript包含了几个progammig图,并带领我们采用多种方式。 我现在很困惑,这是FRP和单向数据流的正确选择之一。

我发现这也使我感到困惑。 只是在这里发布我的用例/解决方案。

我有三个子归约器,它们处理三种不同资源类型(客户端,项目,作业)的数组。 删除客户端( REMOVE_CLIENT )时,必须删除所有相关项目和作业。 作业通过一个项目与客户相关,因此,作业简化程序必须有权访问项目以执行联接逻辑。 作业简化程序必须获取属于要删除的客户端的所有项目ID的列表,然后从存储中删除任何匹配的作业。

我使用以下中间件解决了这个问题:

export default store => next => action => {
  next({ ...action, getState: store.getState });
}

这样,每个操作都可以通过action.getState()访问根存储。

@ rhys-vdw对此表示感谢! 一个非常有用的中间件:)

@ rhys-vdw谢谢您-似乎是一个不错的解决方案。 您如何处理极端情况/参照完整性,例如,当一个实体被两个(或多个)其他实体共享或在删除过程中指向不存在的记录时。 只是想听听您对此的想法。
@gaearon Redux中是否有记录的方法如何解决标准化实体的这种参照完整性问题?

没有具体方法。 我可以想象可以通过定义一个架构并基于该架构生成化简器来实现这一点。 然后,他们将“知道”删除内容后如何收集“垃圾”。 但是,这都是非常麻烦的事情,需要执行工作:-)。 就我个人而言,我认为删除的频率不足以保证真正的清除。 我通常会在模型上翻转status字段,并确保在选择它们时不显示已删除的项目。 或者,当然,如果这不是常见操作,则可以在删除时刷新。

@gaearon为超级快速的回复要么有一个纯粹的状态,没有任何实体的垃圾漂浮。 不知道这些幽灵条目是否能够使您绊倒,特别是当您在状态上有很多CRUD操作时。

但是我也认为,如果Redux同时支持标准化和嵌入式实体,那将是王牌。 而且可以选择一种适合目的的策略。 但是我猜对于嵌入式实体,必须使用nested reducers ,这是Redux文档中的反模式。

我发现很难在reducer中访问其余的存储状态。 在某些情况下,它使嵌套的CombineReducers确实很难使用。 如果有访问其他状态的方法(如.parent().root()或同等方法),那将是很好的。

访问减速器中其他减速器的状态通常是一种反模式。
通常可以通过以下方法解决:

  • 从减速器中删除该逻辑并将其移至选择器;
  • 将更多信息传递给行动;
  • 让视图代码执行两个操作。

谢谢! 我同意这一点。 我只是发现尝试将根状态传递给reducers后变得更加复杂:-)

是的,传递根状态的问题是异化器彼此耦合成彼此的状态形状,这会使状态结构的任何重构或更改变得复杂。

好的,我清楚地看到了拥有规范化状态的好处,并将状态的redux部分像DB一样对待。

我仍在考虑@gaearon(标记要删除的实体)或@ rhys-vdw的方法是否更好(解析参考文献并删除相关参考文献)

有什么想法/现实世界的经验吗?

如果redux更像是榆树,并且状态未规范化,则可以将reducer从状态形状中分离出来。 但是,由于状态已归一化,因此缩减程序似乎经常需要访问应用程序状态的其他部分。

由于Reducer依赖于操作,因此Reducer已经与应用程序可传递地耦合。 您不能在另一个redux应用程序中重用reducer。

通过将状态访问推入操作并使用中间件来解决该问题,这似乎是一种更丑陋的耦合形式,然后允许reduce直接访问根状态。 现在,原本不需要它的动作也被耦合到状态形状,并且比减速器有更多的动作,并且有更多的更改空间。

使状态进入动作可能更好地隔离了状态形状变化,但是如果在添加功能时需要附加状态,则需要更新许多动作。

恕我直言,允许减速器访问根状态将导致比当前变通办法更少的整体耦合。

@kurtharriger :请注意,尽管常见的推荐模式是按域构造Redux状态,并使用combineReducers分别管理每个域,但这只是一个建议。 完全有可能编写自己的顶级化简器,以不同方式管理事物,包括将整个状态传递给特定的子化简器功能。 最终,您实际上只拥有_one _ reducer函数,而如何在内部将其分解完全取决于您。 combineReducers只是一个常见用例的有用实用程序。

关于拆分业务逻辑的Redux FAQ答案也对此主题进行了很好的讨论。

是的,另一种选择是只在一个巨型函数中使用单个reducer和/或编写您自己的CombineReducer函数,而不是使用内置的。
具有一个添加额外的根状态参数的内置CombineReducers方法可能会很有用,但是尝试添加此功能的PR已作为反模式关闭。 我只是想指出,提议的恕我直言的替代方案会导致更多的整体耦合,也许这不应被视为反模式。 另外,如果将根状态添加为附加参数,则它与您被迫使用的方式不同,因此耦合仍然是选择加入的。

我们希望这种耦合是明确的。 手动执行时很明确,它实际上是N行代码,其中N是您拥有的减速器数量。 只是不要使用CombineReducers并手动编写该父reducer。

我是redux / react的新手,但会谦虚地问这个问题:如果组成的化合器访问彼此的状态是一种反模式,因为它在状态形状的不同区域之间增加了耦合,我会认为这种耦合已经在mapStateToProps( ),因为该函数提供了根状态,并且组件从任何地方/任何地方拉来获取其道具。

如果要封装状态子树,则组件应该必须通过隐藏这些子树的访问器访问状态,以便可以在不重构的情况下管理这些作用域内部的状态形状。

当我学习redux时,我正在为自己的应用程序长期存在的问题而苦苦挣扎,这是应用程序之间状态状态的紧密耦合-不是通过reducers而是通过mapStateToProps-例如必须知道auth用户名(例如,每个用户都通过注释reducer与auth reducer进行简化。)我正在尝试将所有状态访问都包装在执行这种封装的访问器方法中,即使在mapStateToProps中,但感觉好像是我在说错了树。

@jwhiting :这是使用“选择器函数”从状态中提取所需数据的多个目的之一,尤其是在mapStateToProps 。 您可以隐藏形状的状态,以使实际需要知道state.some.nested.field的位置的数量最少。 如果尚未看到,请查看Reselect实用程序库,以及Redux文档的“计算派生数据”部分。

是的如果您的数据量很小,但您也不必使用“重新选择”,但仍可以使用(手写)选择器。 查看此方法的shopping-cart示例。

@markerikson @gaearon很有道理,我会探讨的,谢谢。 在mapStateToProps中将选择器用于该组件的主要关注点之外的任何内容(或可能用于所有状态访问),将使组件免受异域状态变化的影响。 不过,我想找到一种方法使状态封装在我的项目中得到更严格的执行,以使代码规范不是唯一的保护措施,因此欢迎提出建议。

将所有代码都保存在单个reducer中并不是很好的分离
关注。 如果减速器是由根调用的,则可以手动调用它
并按照我的建议明确传递了根状态,但是如果您
父级减速器层次结构包含CombineReducers,您必须对其进行翻录
出来。 那么,为什么不使其变得更加复杂,以至于您无需撕裂它
后来还是像大多数人一样重击?
2016年4月16日星期六,晚上8:27 Josh Whiting [email protected]
写道:

@markerikson https://github.com/markerikson @gaearon
https://github.com/gaearon有意义,我将对此进行探讨,
谢谢。 在mapStateToProps中使用选择器来处理其他内容
组件的主要关注点(或者可能是所有状态访问)将屏蔽
异物形状变化引起的分量。 我想找到一种方法
为了使状态封装在我的项目中得到更严格的执行,
因此,代码规范不是唯一的保护措施,因此值得欢迎
那里的建议。

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件或在GitHub上查看
https://github.com/reactjs/redux/issues/601#issuecomment -210940305

@kurtharriger :我们都同意,将combineReducers作为内置工具提供给常见用例。 如果您不想使用该实用程序的当前存在,则不必-完全欢迎您以自己的方式构造化简器,无论是手工编写所有内容还是编写自己的combineReducers变体

由于我们是在讨论模式和关注点分离,因此我实际上遇到了一些相关的问题,但采取了行动。

即,我在各地看到的ajax shouldFetch模式似乎与状态形状紧密相关。 例如,这里。 在该示例中,如果我重新使用化简器,但在状态的根部没有实体对象怎么办? 该操作将失败。 这里有什么建议? 我应该添加针对案例的操作吗? 还是接受选择器的动作?

@shadowii让动作创建者导入选择器对我来说似乎很好。 我会将选择器与reducer并置,以便将关于状态形状的知识本地化在这些文件中。

有不同的异径管处理相同的动作类型有什么问题?
例如,如果我有一个account和一个auth减速器,它们都处理LOGIN_SUCCESS动作类型(在有效负载中带有auth令牌和帐户数据),每个一个只更新他们管理的状态片。 当动作影响到州的不同部分时,我发现自己已经使用这种方法很多次了。

有不同的异径管处理相同的动作类型有什么问题?

没问题,这是一种非常常见且有用的模式。 实际上,存在_reason_动作:如果将动作映射到减速器1:1,则完全没有任何意义。

@ rhys-vdw我尝试使用您发布的中间件。

customMiddleare.js

const customMiddleware =  store => next => action => {
  next({ ...action, getState: store.getState });
};

在我的商店创建中

import customMiddleware from './customMiddleware';

var store = createStore(
        rootReducer,
        initialState,
        applyMiddleware(
            reduxPromise,
            thunkMiddleware,
            loggerMiddleware,
            customMiddleware
        )
    );
return store;

我收到以下错误:

applyMiddleware.js?ee15:49未被捕获的TypeError:中间件不是函数(…)
(匿名函数)@ applyMiddleware.js?ee15:49
(匿名函数)@ applyMiddleware.js?ee15:48
createStore @ createStore.js?fe4c:65
configureStore @ configureStore.js? ffde:45
(匿名函数)@ index.jsx?fdd7:25
(匿名函数)@ bundle.js:1639
webpack_require @ bundle.js:556
fn @ bundle.js:87(匿名函数)@ bundle.js:588
webpack_require @ bundle.js:556
(匿名函数)@ bundle.js:579
(匿名函数)@ bundle.js:582

@ mars76 :很可能您未正确导入某些内容。 如果那确实是customMiddleware.js的全部内容,那么您就不会导出_anything_。 通常,您要将四个中间件传递给applyMiddleware ,并且大概其中之一不是有效的引用。 退后一步,将其全部删除,一次重新添加一个,查看哪个无效,并找出原因。 检查导入和导出语句,仔细检查如何初始化事物,等等。

你好

谢谢@markerikson

我将语法转换如下,现在可以了:

customMiddleware.js

export function customMiddleware(store) { return function (next) { return function (action) { next(Object.assign({}, action, { getState: store.getState })); }; }; } console.log(customMiddleware);

我如何尝试在Action Creator本身(而不是Reducer中)中访问商店。 我需要进行异步调用,并且需要传递由各种reducer管理的商店状态的不同部分。

这就是我在做什么。 不确定这是否是正确的方法。

我使用数据调度动作,并且在reducer内部,我现在可以访问整个状态,并且我正在提取所需的必要部分并创建对象,然后使用此数据调度另一个动作,该动作将在我的动作创建者中可用正在使用该数据进行Asyn调用。

我最大的担心是Reducers现在正在调用动作创建者方法。

感谢任何评论。

谢谢

@ mars76 :同样,您不应该_不要_尝试从Reducer中调度。

要访问动作创建者内部的商店,可以使用redux-thunk中间件。 您可能还需要阅读FAQ有关在reducer之间共享数据常见的thunk用法示例编写的要点。

非常感谢@markerikson。

我的糟糕,我没有意识到redux-thunk中的getState()。 这使我的工作更加轻松,并使减速器保持纯净。

@gaearon,当您提到让动作创建者导入选择器时,您是否想将整个状态传递给动作创建者以移交给选择器,还是我遗漏了什么?

@tacomanator :仅供参考,这些天,Dan通常不会对Redux问题中的随机ping做出响应。 其他优先事项太多。

在大多数情况下,如果您在动作创建者中使用选择器,那么您会在重击过程中进行选择。 Thunk可以访问getState ,因此可以访问整个状态树:

import {someSelector} from "./selectors";

function someThunk(someParameter) {
    return (dispatch, getState) => {
        const specificData = someSelector(getState(), someParameter);

        dispatch({type : "SOME_ACTION", payload : specificData});    
    }
}

@markerikson谢谢,这很有道理。 仅供参考,我要问的原因主要是关于使用选择器派生的数据来验证业务逻辑,而不是将其分配给reducer,但这也让我深思。

我用来将减速器组合成一个的快速技巧是

const reducers = [ reducerA, reducerB, ..., reducerZ ];

let reducer = ( state = initialState, action ) => {
    return reducers.reduce( ( state, reducerFn ) => (
        reducerFn( state, action )
    ), state );
};

其中reducerAreducerZ是单个化简函数。

@brianpkelley :是的,基本上就是https://github.com/acdlite/reduce-reducers

@markerikson很好,第一次使用react / redux / etc并在调查此问题时发现了该线程。 通过大声笑跳到末尾约1/2

嘿,当然。

仅供参考,您可能需要通读FAQ:“我必须使用combineReducers吗?” 文档中的结构化Reducers部分。 另外,我的“ Practical Redux”教程系列

使用redux-logic时,我的两分钱是在transform函数中从根状态增加操作。

例:

const formInitLogic = createLogic({
  type: 'FORM_INIT',
  transform({ getState, action }, next) {
    const state = getState();
    next({
      ...action,
      payload: {
        ...action.payload,
        property1: state.branch1.someProperty > 5,
        property2: state.branch1.anotherProperty + state.branch2.yetAnotherProperty,
      },
    });
  },
});

我认为这更多是关于代码结构:
“不要直接访问子状态操作”
在许多项目中使用redux之后,我发现最好的代码结构是将每个子状态作为一个完全分离的组件来处理,这些组件中的每个组件都有其自己的文件夹和逻辑,要实现这一想法,我使用以下文件结构:

  • 还原

    • reducers.js导出来自每个州的减速器

    • middleware.js导出所有中间件的applyMiddleware

    • configureStore.js createStore使用(reducers.js,middleware.js)

    • actions.js导出从状态文件夹中分派动作的动作<== THIS

    • 状态1

    • initial.js此文件包含初始状态(我使用不可变来创建记录)

    • action-types.js此文件包含操作类型(我使用dacho作为键镜像)

    • actions.js此文件包含状态操作

    • reducer.js此文件包含状态reducer

    • 状态2

    • initial.js

    • action-types.js

      ....

为什么呢
1-每个应用程序中有两种类型的操作:

  • 状态操作(redux / state1 / actions.js),这些操作仅负责更改状态,您不应该直接使用应用操作来直接触发这些操作

    • 应用程式动作(redux / actions.js),这些动作负责执行某些应用程式逻辑,并可以触发状态动作

2-另外使用这种结构,您可以通过复制粘贴文件夹并重命名它来创建新状态;)

当2个异径管需要更改相同属性时该怎么办?

假设我有一个减速器,我想将其拆分为不同的文件,并且有一个名为“错误”的属性,当HTTP请求由于某种原因失败时,我会对其进行更改。

现在,减速器已经变大了,所以我决定将其拆分为几个减速器,但是所有这些减速器都需要更改该“错误”属性。

那呢

@ Wassap124两个减速器无法管理相同的状态。 您是说两个动作需要更改同一属性吗?

如果没有更多详细信息,您可能更喜欢使用thunk来调度“设置错误”操作。

@ rhys-vdw我只是想尝试拆分一个相当长的减速器而被卡住。
关于您的建议,这是否与没有副作用的规则相抵触?

@ Wassap124vdw :澄清一下,如果您仅使用combineReducers _,则两个reducer不可能管理相同的状态。 但是,您当然可以编写其他reducer逻辑以适合您自己的用例-请参阅https://redux.js.org/recipes/structuringreducers/beyondcombinereducers

如果您进行一些快速的来源拼写...

combineReducers返回一个函数签名,如下所示

return function combination(state = {}, action) {

参见https://github.com/reduxjs/redux/blob/master/src/combineReducers.js#L145

创建一个闭包以引用实际的reducers对象后。

因此,您可以添加诸如此类的简单操作,以便在需要充水并从存储中重新加载状态时全局处理操作。

const globalReducerHandler = () => {
  return (state = {}, action) => {
    if (action.type === 'DO_GLOBAL_THING') {
      return globallyModifyState(state)
    }
    return reducers(state, action)
  }
}

这可能不是反模式,但实用主义一直更加重要。

与其从另一个减速器调用减速器(这是一种反模式,因为globalReducerHandler与它调用的减速器无关),请使用reduceReducers ,对于减速器本质上是compose (嗯,它是从字面上构成(Action ->) monad)。

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

相关问题

benoneal picture benoneal  ·  3评论

mickeyreiss-visor picture mickeyreiss-visor  ·  3评论

CellOcean picture CellOcean  ·  3评论

ilearnio picture ilearnio  ·  3评论

vraa picture vraa  ·  3评论