Комбинируйте редукторы сокращения без добавления гнездования

У меня есть сценарий, в котором у меня есть 2 редуктора, которые являются результатом combineReducers. Я хочу объединить их вместе, но держу их ключи на одном уровне при вложении.

Например, учитывая следующие редукторы

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

Я хочу закончить со структурой как:

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

Если я снова использую combineReducers с reducerA на reducerA и reducerB вот так:

const reducer = combineReducers({ reducerA, reducersB })

Я в конечном итоге со структурой, как:

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

Я не могу совмещать reducerA1, reducerA2, reducerB1 и reducerB2 в едином combineReducers называют reducerA и reducerB которые были предоставлено мне уже сложены из различных пакетов НПХ.

Я попытался использовать библиотеку Reducer-Redurs, чтобы объединить их вместе и уменьшить состояние вместе, идея, которую я получил, глядя на документы DX, вот так:

const reducer = reduceReducers(reducerA, reducerB)

К сожалению, это не сработало, так как результирующий редуктор от combineReducers предупреждение, если неизвестные ключи найдены, и игнорирует их при возврате своего состояния, поэтому результирующая структура содержит только структуру reducerB:

{
    reducerB1: ...,
    reducerB2: ...
}

На самом деле я не хочу реализовывать свой собственный combineReducers, который не строго combineReducers за структурой, если мне это не нужно, поэтому я надеюсь, что кто-то знает о другом способе, либо встроенном в redux, либо из библиотеки, которая может помоги мне с этим. Есть идеи?


Редактировать:

Был получен ответ (кажется, что он был удален), в котором предлагалось использовать библиотеку flat-Объединение-редукторы:

const reducer = flatCombineReducers(reducerA, reducerB)

Это было на один шаг ближе к редукторам в том reducerB, что ему удавалось сохранять состояние сохранения как для reducerA и для reducerB, но все еще создаются предупреждающие сообщения, что заставляет меня задаться вопросом, не было ли в состоянии исчезновения, которое я наблюдал ранее, не combineReducers далеко, а скорее что-то еще происходит с реализацией редукторов.

Предупреждающие сообщения:

Неожиданные ключи "reducerB1", "reducerB2", найденные в предыдущем состоянии, получены редуктором. Ожидается, что вместо этого будет найден один из известных ключей редуктора: "reducerA1", "reducerA2". Неожиданные ключи будут игнорироваться.

Неожиданные ключи "reducerA1", "reducerA2", найденные в предыдущем состоянии, получены редуктором. Ожидается, что вместо этого будет найден один из известных ключей редуктора: "reducerB1", "reducerB2". Неожиданные ключи будут игнорироваться.

Если я сделаю производственную сборку, предупреждение исчезнет (таков путь для многих предупреждений о реакции/избыточности), но я бы предпочел, чтобы они вообще не появлялись.

Я также сделал еще несколько поисков для других библиотек и нашел redux-concatenate-redurs:

const reducer = concatenateReducers([reducerA, reducerB])

Это имеет тот же результат, что и плоские комбайны-редукторы, поэтому поиск продолжается.


Изменить 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 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 проходит все тесты, но по-прежнему 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 редуктора (как выше), массив редукторов или объект редукторов (например, combineReducers). На самом деле, я не возражаю против того, как это достигается, если не требуется никаких изменений в создании reducerA reducerB, reducerA1 reducerA1, reducerB1 reducerA, reducerB reducerA1, reducerA1 reducerB1 или reducerB2.


Изменить 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-redurs (возможно, использовались flat-Объединить-редукторы, но реализация первого слияния кажется немного более надежной). Функция mergeReducers выглядит следующим образом:

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

Это называется так:

const store = createStore(mergeReducers(reducerA, reducerB)

Это проходит все тесты и не combineReducers никаких предупреждений от редукторов, созданных с помощью combineReducers.

Единственный бит, в котором я не уверен, - это где массив knownKeys вызовом редуктора с действием INIT. Это работает, но кажется немного грязным. Если я этого не сделаю, то выдается только предупреждение, если хранилище создано с начальным состоянием (дополнительные ключи не отфильтровываются при разрешении начального состояния редуктора).

Ответ 1

Хорошо, решил сделать это для удовольствия, не слишком много кода... Это будет обернуть редуктор и предоставить только ключи, которые он сам вернул.

// 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 }))

ПРИМЕЧАНИЕ.. Это нарушает идею о том, что редуктор всегда будет обеспечивать одинаковый выход с теми же входами. Вероятно, было бы лучше принять список ключей при создании редуктора:

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

Ответ 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);
        };
    };

Предыдущий ответ:

Чтобы связать массив редукторов, можно использовать следующую функцию:

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

Чтобы объединить несколько редукторов, просто используйте его следующим образом:

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

Ответ 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)

Это не дает ошибок. Я в основном возвращаю сглаженные выходные combineReducers функции combineReducers -неизменяя функции combineReducers.

Я также выпустил это как пакет npm здесь: redux-immutable-merge-redurs.