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

时间:2017-04-08 04:14:52

标签: javascript redux

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

例如,给出以下减速器

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

我希望最终得到一个结构:

{
    reducerA1: ...,
    reducerA2: ...,
    reducerB1: ...,
    reducerB2: ...
}

如果我在combineReducersreducerA再次使用reducerB,请执行以下操作:

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库:

const reducer = flatCombineReducers(reducerA, reducerB)

这比reduce-reducers更接近一步,因为它设法保持状态不受reducerAreducerB的影响,但警告消息仍在产生,这让我想知道是否我之前观察到的消失状态不是combineReducers抛弃它,而是还有其他一些与reduce-reducers实现相关的东西。

警告信息为:

  

意外键“reducerB1”,“reducerB2”在减速器收到的先前状态中找到。预计会找到一个已知的减速器键:“reducerA1”,“reducerA2”。意外的密钥将被忽略。

     

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

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

我还做了一些其他库的搜索,发现了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)
    })
})

目标是让此测试通过 AND 在控制台中不产生任何警告。

编辑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减速器只会打印一次唯一的警告,所以因为我在测试之间重复使用减速器,警告是仅显示第一个测试用例来生成它(我可以在每个测试中结合使用减速器来防止这种情况,但作为我正在寻找它的标准,根本不能生成它们,我现在对此很满意)。

如果你正在尝试这个,我不介意mergeReducers是否接受2个reducer(如上所述),reducers数组或reducers对象(如combineReducers)。实际上,我不介意它是如何实现的,只要它不需要对reducerAreducerBreducerA1reducerA1的创建进行任何更改,{ {1}}或reducerB1

编辑4:

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

我们的想法是使用以下包装器使用先前调用的键过滤提供给reducer的状态:

reducerB2

我使用redux-concatenate-reducers库合并了滤波后的reducer的结果(可能使用了flat-combine-reducers,但前者的合并实现看起来更健壮了)。 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; }; } 函数如下所示:

mergeReducers

这就是这样称呼的:

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

这会传递所有测试,并且不会使用const store = createStore(mergeReducers(reducerA, reducerB) 创建的缩减器产生任何警告。

我不确定的唯一一点是通过combineReducers动作调用reducer来播种knownKeys数组的位置。它有效,但感觉有点脏。如果我不这样做,那么产生的唯一警告是如果创建存储的初始状态(在解析reducer的初始状态时不会过滤掉额外的键。

4 个答案:

答案 0 :(得分:3)

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

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

在你的测试中:

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'])

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

import datetime
date_time = datetime.datetime.now()

date = date_time.date()  # Gives the date
time = date_time.time()  # Gives the time

print date.year, date.month, date.day
print time.hour, time.minute, time.second, time.microsecond

答案 1 :(得分: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]);

答案 2 :(得分:1)

使用不可变的解决方案

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

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

答案 3 :(得分:0)

也有合并的Reduction Reducer实用程序

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