在不添加嵌套的情况下组合 redux reducer

Posted

技术标签:

【中文标题】在不添加嵌套的情况下组合 redux reducer【英文标题】:Combine redux reducers without adding nesting 【发布时间】:2017-09-03 13:05:54 【问题描述】:

我有一个场景,我有 2 个减速器,它们是 combineReducers 的结果。我想将它们组合在一起,但在嵌套时将它们的键保持在同一级别。

例如,给定以下减速器

const reducerA = combineReducers( reducerA1, reducerA2 )
const reducerB = combineReducers reducerB1, reducerB2 )

我想最终得到这样的结构:


    reducerA1: ...,
    reducerA2: ...,
    reducerB1: ...,
    reducerB2: ...

如果我在reducerAreducerB 上再次使用combineReducers,就像这样:

const reducer = combineReducers( reducerA, reducersB )

我最终得到如下结构:


    reducerA: 
        reducerA1: ...,
        reducerA2: ...
    ,
    reducerB: 
        reducerB1: ...,
        reducerB2: ...
    

我无法将 reducerA1reducerA2reducerB1reducerB2 组合在一个 combineReducers 调用中,因为 reducerAreducerB 已经从不同的 npm 包提供给我。

我尝试使用reduce-reducers 库将它们组合在一起并将状态减少在一起,这是我从redux docs 中得到的一个想法,如下所示:

const reducer = reduceReducers(reducerA, reducerB)

不幸的是,这不起作用,因为来自combineReducers生产者的reducer会在找到未知键时发出警告,并在返回其状态时忽略它们,因此结果结构仅包含reducerB的结构:


    reducerB1: ...,
    reducerB2: ...

我真的不想实现我自己的combineReducers,如果我不需要,它不会严格执行结构,所以我希望有人知道另一种方式,或者内置到 redux 或来自可以帮助我的图书馆。有什么想法吗?


编辑:

提供了一个答案(现在似乎已被删除)建议使用flat-combine-reducers library:

const reducer = flatCombineReducers(reducerA, reducerB)

这比 reduce-reducers 更近了一步,因为它设法保持了 reducerAreducerB 的状态,但仍在产生警告消息,这让我想知道消失的状态是否我之前观察到的不是combineReducers 扔掉它,而是reduce-reducers 实现发生的其他事情。

警告信息是:

在reducer 接收到的先前状态中发现意外的键“reducerB1”、“reducerB2”。预计会找到一个已知的 reducer 键:“reducerA1”、“reducerA2”。意外的键将被忽略。

在reducer 接收到的先前状态中发现意外的键“reducerA1”、“reducerA2”。预计会找到已知的 reducer 键之一:“reducerB1”、“reducerB2”。意外的键将被忽略。

如果我进行生产构建,警告会消失(这是许多 react/redux 警告的方式),但我宁愿它们根本不出现。

我还搜索了其他库,发现redux-concatenate-reducers:

const reducer = concatenateReducers([reducerA, reducerB])

这与 flat-combine-reducers 具有相同的结果,因此搜索会继续。


编辑 2:

现在有一些人提出了一些建议,但到目前为止都没有奏效,所以这里有一个测试可以提供帮助:

import  combineReducers, createStore  from 'redux'

describe('Sample Tests', () => 

    const reducerA1 = (state = 0) => state
    const reducerA2 = (state =  test: "value1") => state
    const reducerB1 = (state = [ "value" ]) => state
    const reducerB2 = (state =  test: "value2") => state
    
    const reducerA = combineReducers( reducerA1, reducerA2 )
    const reducerB = combineReducers( reducerB1, reducerB2 )

    const mergeReducers = (...reducers) => (state, action) => 
        return /* your attempt goes here */
    

    it('should merge reducers', () => 
        const reducer = mergeReducers(reducerA, reducerB)

        const store = createStore(reducer)

        const state = store.getState()

        const expectedState = 
            reducerA1: 0,
            reducerA2: 
                test: "value1"
            ,
            reducerB1: [ "value" ],
            reducerB2: 
                test: "value2"
            
        

        expect(state).to.deep.equal(expectedState)
    )
)

目标是让这个测试通过并且不在控制台中产生任何警告。


编辑 3:

添加了更多测试以涵盖更多情况,包括在初始创建后处理操作以及是否使用初始状态创建商店。

import  combineReducers, createStore  from 'redux'

describe('Sample Tests', () => 

    const reducerA1 = (state = 0) => state
    const reducerA2 = (state =  test: "valueA" ) => state
    const reducerB1 = (state = [ "value" ]) => state
    const reducerB2 = (state = , action) => action.type == 'ADD_STATE' ?  ...state, test: (state.test || "value") + "B"  : state
    
    const reducerA = combineReducers( reducerA1, reducerA2 )
    const reducerB = combineReducers( reducerB1, reducerB2 )

    // from Javaguru's answer
    const mergeReducers = (reducer1, reducer2) => (state, action) => (
        ...state,
        ...reducer1(state, action),
        ...reducer2(state, action)
    )

    it('should merge combined reducers', () => 
        const reducer = mergeReducers(reducerA, reducerB)

        const store = createStore(reducer)

        const state = store.getState()

        const expectedState = 
            reducerA1: 0,
            reducerA2: 
                test: "valueA"
            ,
            reducerB1: [ "value" ],
            reducerB2: 
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge basic reducers', () => 
        const reducer = mergeReducers(reducerA2, reducerB2)

        const store = createStore(reducer)

        const state = store.getState()

        const expectedState = 
            test: "valueA"
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge combined reducers and handle actions', () => 
        const reducer = mergeReducers(reducerA, reducerB)

        const store = createStore(reducer)

        store.dispatch( type: "ADD_STATE" )

        const state = store.getState()

        const expectedState = 
            reducerA1: 0,
            reducerA2: 
                test: "valueA"
            ,
            reducerB1: [ "value" ],
            reducerB2: 
                test: "valueB"
            
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge basic reducers and handle actions', () => 
        const reducer = mergeReducers(reducerA2, reducerB2)

        const store = createStore(reducer)

        store.dispatch( type: "ADD_STATE" )

        const state = store.getState()

        const expectedState = 
            test: "valueAB"
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge combined reducers with initial state', () => 
        const reducer = mergeReducers(reducerA, reducerB)

        const store = createStore(reducer,  reducerA1: 1, reducerB1: [ "other" ] )

        const state = store.getState()

        const expectedState = 
            reducerA1: 1,
            reducerA2: 
                test: "valueA"
            ,
            reducerB1: [ "other" ],
            reducerB2: 
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge basic reducers with initial state', () => 
        const reducer = mergeReducers(reducerA2, reducerB2)

        const store = createStore(reducer,  test: "valueC" )

        const state = store.getState()

        const expectedState = 
            test: "valueC"
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge combined reducers with initial state and handle actions', () => 
        const reducer = mergeReducers(reducerA, reducerB)

        const store = createStore(reducer,  reducerA1: 1, reducerB1: [ "other" ] )

        store.dispatch( type: "ADD_STATE" )

        const state = store.getState()

        const expectedState = 
            reducerA1: 1,
            reducerA2: 
                test: "valueA"
            ,
            reducerB1: [ "other" ],
            reducerB2: 
                test: "valueB"
            
        

        expect(state).to.deep.equal(expectedState)
    )

    it('should merge basic reducers with initial state and handle actions', () => 
        const reducer = mergeReducers(reducerA2, reducerB2)

        const store = createStore(reducer,  test: "valueC" )

        store.dispatch( type: "ADD_STATE" )

        const state = store.getState()

        const expectedState = 
            test: "valueCB"
        

        expect(state).to.deep.equal(expectedState)
    )
)

上述mergeReducers 实现通过了所有测试,但仍向控制台发出警告。

  Sample Tests
    ✓ should merge combined reducers
    ✓ should merge basic reducers
Unexpected keys "reducerB1", "reducerB2" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "reducerA1", "reducerA2". Unexpected keys will be ignored.
Unexpected keys "reducerA1", "reducerA2" found in previous state received by the reducer. Expected to find one of the known reducer keys instead: "reducerB1", "reducerB2". Unexpected keys will be ignored.
    ✓ should merge combined reducers and handle actions
    ✓ should merge basic reducers and handle actions
    ✓ should merge combined reducers with initial state
    ✓ should merge basic reducers with initial state
    ✓ should merge combined reducers with initial state and handle actions
    ✓ should merge basic reducers with initial state and handle actions

重要的是要注意,正在打印的警告是针对测试用例之后的,combineReducers reducers 只会打印每个唯一的警告一次,所以因为我在测试之间重用了 reducer,所以只显示警告对于第一个测试用例来生成它(我可以在每个测试中结合减速器来防止这种情况,但作为我正在寻找它根本不生成它们的标准,我现在对此感到满意)。

如果您尝试这样做,我不介意 mergeReducers 是否接受 2 个减速器(如上)、一个减速器数组或一个减速器对象(如 combineReducers)。实际上,我不介意它是如何实现的,只要它不需要对 reducerAreducerBreducerA1reducerA1reducerB1reducerB2 的创建进行任何更改。


编辑 4:

我当前的解决方案是根据 Jason Geomaat 的回答修改的。

这个想法是通过使用以下包装器使用先前调用的键来过滤提供给减速器的状态:

export const filteredReducer = (reducer) => 
    let knownKeys = Object.keys(reducer(undefined,  type: '@@FILTER/INIT' ))

    return (state, action) => 
        let filteredState = state

        if (knownKeys.length && state !== undefined) 
            filteredState = knownKeys.reduce((current, key) => 
                current[key] = state[key];
                return current
            , )
        

        let newState = reducer(filteredState, action)

        let nextState = state

        if (newState !== filteredState) 
            knownKeys = Object.keys(newState)
            nextState = 
                ...state,
                ...newState
            
        

        return nextState;
    ;

我使用 redux-concatenate-reducers 库合并过滤后的 reducer 的结果(可以使用 flat-combine-reducers,但前者的合并实现似乎更健壮一些)。 mergeReducers 函数看起来像:

const mergeReducers = (...reducers) => concatenateReducers(reducers.map((reducer) => filterReducer(reducer))

这是这样称呼的:

const store = createStore(mergeReducers(reducerA, reducerB)

这通过了所有测试,并且不会从使用combineReducers 创建的减速器产生任何警告。

我不确定的唯一一点是 knownKeys 数组在哪里播种,方法是使用 INIT 操作调用减速器。它有效,但感觉有点脏。如果我不这样做,则产生的唯一警告是如果商店是使用初始状态创建的(解析减速器的初始状态时不会过滤掉额外的键。

【问题讨论】:

最简单的方法不是: const combinedReducersAB = (state, action) => reducerB(reducerA(state, action), action); ?? 这和你的回答有同样的问题。 你好@MichaelPeyper,这个解决方案还能应用吗?我想知道这个解决方案是否解决了n 级别的问题,还是可以解决超过 2 个组合减速器? 我们将它用于深度嵌套的结构,每个级别有超过 2 个减速器,没有任何问题。 【参考方案1】:

好的,决定这样做是为了好玩,而不是太多代码...这将包装一个 reducer,并且只提供它自己返回的键。

// don't provide keys to reducers that don't supply them
const filterReducer = (reducer) => 
  let lastState = undefined;
  return (state, action) => 
    if (lastState === undefined || state == undefined) 
      lastState = reducer(state, action);
      return lastState;
    
    var filteredState = ;
    Object.keys(lastState).forEach( (key) => 
      filteredState[key] = state[key];
    );
    var newState = reducer(filteredState, action);
    lastState = newState;
    return newState;
  ;

在你的测试中:

const reducerA = filterReducer(combineReducers( reducerA1, reducerA2 ))
const reducerB = filterReducer(combineReducers( reducerB1, reducerB2 ))

注意:这确实打破了减速器在给定相同输入的情况下始终提供相同输出的想法。在创建 reducer 时接受键列表可能会更好:

const filterReducer2 = (reducer, keys) => 
  let lastState = undefined;
  return (state, action) => 
    if (lastState === undefined || state == undefined) 
      lastState = reducer(state, action);
      return lastState;
    
    var filteredState = ;
    keys.forEach( (key) => 
      filteredState[key] = state[key];
    );
    return lastState = reducer(filteredState, action);
  ;


const reducerA = filterReducer2(
  combineReducers( reducerA1, reducerA2 ),
  ['reducerA1', 'reducerA2'])
const reducerB = filterReducer2(
  combineReducers( reducerB1, reducerB2 ),
  ['reducerB1', 'reducerB2'])

【讨论】:

问题不是复制combineReducers 所做的,而是处理由它创建的reducer。如问题中所述,我需要处理的减速器来自其他 npm 包,并且已经与 combineReducers 结合使用。它们是如何组合的不在我的控制范围内,我不会坚持任何人不使用记录良好的内置函数。不行的话没关系,我只好另寻他法了 酷,我马上试试。预先提供密钥的选项是行不通的,因为我不知道减速器的回报是什么。 天哪,这太近了。它失败了'should merge basic reducers and handle actions' 案例,因为它没有合并共享密钥。我可以忍受这种情况,因为常见的情况是从combineReducers 创建的减速器,希望不会有任何共享密钥。 'should merge combined reducers with initial state' 案例仍在产生警告,但同样,这可能是可以接受的,因为商店不太可能以初始状态创建(重点是我不知道确切的状态结构会是什么样子喜欢)。 我真的很喜欢你在每个减速器级别处理过滤的方法,消除了我一直在研究的许多复杂性(实际上与你在这里所拥有的概念相同,但由mergeReducers 包装器管理。我将稍微修改一下,看看我能想出什么。 奇怪,我复制了你的测试来编写它并且都通过了所有测试...... DOH!我忘了我也改变了mergeReducers。好的,奇怪的是,我遇到了一些麻烦,我只是重新复制了您的测试并添加了 filterReducer 并像我的答案一样包装了减速器 A 和 B,所有 8 个测试再次通过...【参考方案2】:

好的,虽然此时问题已经解决了,但我只是想分享一下我想出的解决方案:

    import  ActionTypes  from 'redux/lib/createStore'

    const mergeReducers = (...reducers) => 
        const filter = (state, keys) => (
          state !== undefined && keys.length ?

            keys.reduce((result, key) => 
              result[key] = state[key];
              return result;
            , ) :

            state
        );

        let mapping  = null;

        return (state, action) => 
            if (action && action.type == ActionTypes.INIT) 
                // Create the mapping information ..
                mapping = reducers.map(
                  reducer => Object.keys(reducer(undefined, action))
                );
            
            return reducers.reduce((next, reducer, idx) => 
                const filteredState = filter(next, mapping[idx]);
                const resultingState = reducer(filteredState, action);

                return filteredState !== resultingState ?
                  ...next, ...resultingState : 
                  next;

            , state);
        ;
    ;

上一个答案:

为了链接一个reducer数组,可以使用以下函数:

const combineFlat = (reducers) => (state, action) => reducers.reduce((newState, reducer) => reducer(newState, action), state));

为了组合多个reducer,简单的使用如下:

const combinedAB = combineFlat([reducerA, reducerB]);

【讨论】:

您的combineFlat 函数落入了与reduce-reducers 相同的陷阱,其中状态保留为最终reducer 的状态。 抱歉,我刚刚看到您的更新(堆栈溢出不会通知是否有人适合并回答您的问题)。我稍后会尝试,但我认为如果调度第二个操作或者如果使用初始状态创建存储,因为传入的状态具有组合减速器的意外键,它将有一个警告。 正如我所怀疑的,这在创建初始状态之前一直有效,但是如果在创建商店时提供了初始状态或发送了一个操作,则会出现警告。我已经用更多测试更新了这个问题以涵盖这些情况。 @MichaelPeyper:我根据您的测试更新了答案。它也应该在调度动作期间/之后工作。 这与this answer 在同一条船上 - 失败'should merge basic reducers and handle actions' 并在'should merge combined reducers with initial state' 上产生警告。这两种情况对我来说都不那么重要,但如果可以的话,我想解决它们。【参考方案3】:

使用 Immutable 的解决方案

上面的解决方案不处理不可变存储,这是我偶然发现这个问题时需要的。这是我想出的一个解决方案,希望它可以帮助其他人。

import  fromJS, Map  from 'immutable';
import  combineReducers  from 'redux-immutable';

const flatCombineReducers = reducers => 
  return (previousState, action) => 
    if (!previousState) 
      return reducers.reduce(
        (state = , reducer) =>
          fromJS( ...fromJS(state).toJS(), ...reducer(previousState, action).toJS() ),
        ,
      );
    
    const combinedReducers = combineReducers(reducers);
    const combinedPreviousState = fromJS(
      reducers.reduce(
        (accumulatedPreviousStateDictionary, reducer, reducerIndex) => (
          ...accumulatedPreviousStateDictionary,
          [reducerIndex]: previousState,
        ),
        ,
      ),
    );
    const combinedState = combinedReducers(combinedPreviousState, action).toJS();
    const isStateEqualToPreviousState = state =>
      Object.values(combinedPreviousState.toJS()).filter(previousStateForComparison =>
        Map(fromJS(previousStateForComparison)).equals(Map(fromJS(state))),
      ).length > 0;
    const newState = Object.values(combinedState).reduce(
      (accumulatedState, state) =>
        isStateEqualToPreviousState(state)
          ? 
              ...state,
              ...accumulatedState,
            
          : 
              ...accumulatedState,
              ...state,
            ,
      ,
    );

    return fromJS(newState);
  ;
;

const mergeReducers = (...reducers) => flatCombineReducers(reducers);

export default mergeReducers;

然后这样调用:

mergeReducers(reducerA, reducerB)

它不会产生错误。我基本上是在返回 redux-immutable combineReducers 函数的扁平输出。

我还在这里将它作为 npm 包发布:redux-immutable-merge-reducers。

【讨论】:

【参考方案4】:

还有combinedReduction reducer实用程序

const reducer = combinedReduction(
  migrations.reducer,
  
    session: session.reducer,
    entities: 
      users: users.reducer,
    ,
  ,
);

【讨论】:

以上是关于在不添加嵌套的情况下组合 redux reducer的主要内容,如果未能解决你的问题,请参考以下文章

reducer.state.props 在嵌套动作 react/redux 中未定义

将项目添加到 redux-toolkit 中的嵌套数组

从嵌套对象中删除数据而不改变

Redux: Reduced - 如何在嵌套数组的递归过程中避免对象不可扩展错误?

Redux reducer / state-shape design fordependent state-slices

从redux中的reducer获取ID