【发布时间】:2017-09-03 13:05:54
【问题描述】:
我有一个场景,我有 2 个减速器,它们是 combineReducers 的结果。我想将它们组合在一起,但在嵌套时将它们的键保持在同一级别。
例如,给定以下减速器
const reducerA = combineReducers({ reducerA1, reducerA2 })
const reducerB = combineReducers{{ reducerB1, reducerB2 })
我想最终得到这样的结构:
{
reducerA1: ...,
reducerA2: ...,
reducerB1: ...,
reducerB2: ...
}
如果我在reducerA 和reducerB 上再次使用combineReducers,就像这样:
const reducer = combineReducers({ reducerA, reducersB })
我最终得到如下结构:
{
reducerA: {
reducerA1: ...,
reducerA2: ...
},
reducerB: {
reducerB1: ...,
reducerB2: ...
}
}
我无法将 reducerA1、reducerA2、reducerB1 和 reducerB2 组合在一个 combineReducers 调用中,因为 reducerA 和 reducerB 已经从不同的 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 更近了一步,因为它设法保持了 reducerA 和 reducerB 的状态,但仍在产生警告消息,这让我想知道消失的状态是否我之前观察到的不是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)。实际上,我不介意它是如何实现的,只要它不需要对 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-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 个减速器,没有任何问题。
标签: javascript redux