託 combineReducers
之福,我們可以把 state 分割成多個不同的片段,再合併成一新的 state 樹。但偶爾會遇到這麼一種情境:為了處理某些特殊的 actions,sliceReducerA
需要來自 sliceReducerB
的部分 state 數據,或 sliceReducerB
需要全部的 state 作為參數。combineReducers
無法解決這樣的需求。
function combinedReducer(state, action) {
switch(action.type) {
case "A_TYPICAL_ACTION" : {
return {
a : sliceReducerA(state.a, action),
b : sliceReducerB(state.b, action)
};
}
case "SOME_SPECIAL_ACTION" : {
return {
// 明確地把 state.b 作為额外參數傳遞
a : sliceReducerA(state.a, action, state.b),
b : sliceReducerB(state.b, action)
}
}
case "ANOTHER_SPECIAL_ACTION" : {
return {
a : sliceReducerA(state.a, action),
// 明確地把全部 state 作為额外參數傳遞
b : sliceReducerB(state.b, action, state)
}
}
default: return state;
}
}
此中無深意,就不贅言了。
可以透過 thunk 函數或類似方法,為 action 添加額外數據:
function someSpecialActionCreator() {
return (dispatch, getState) => {
const state = getState();
const dataFromB = selectDataFromB(state);
dispatch({
type: 'SOME_SPECIAL_ACTION',
payload: { dataFromB }
});
}
}
如此 B 的數據已存在於 action 中,故其父級 reducer 就可直接把數據暴露給 sliceReducerA
。
combineReducers
依舊負責組合 reducers 這種簡單場景,再新加一 reducer 來處理多塊數據交叉的複雜場景。最後使用一包裹函數依次調用此二類 reducers 並輸出最終結果:
const combinedReducer = combineReducers({
a: sliceReducerA,
b: sliceReducerB
});
function crossSliceReducer(state, action) {
switch(action.type) {
case 'SOME_SPECIAL_ACTION': {
return {
// 明確地把 state.b 作為额外參數傳遞
a: handleSpecialCaseForA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
};
}
default: return state;
}
}
function rootReducer(state, action) {
const intermediateState = combinedReducer(state, action);
const finalState = crossSliceReducer(intermediateState, action);
return finalState;
}
已有一庫 reduce-reducers 可簡化上述流程(指 上一個解決方案),並把產生的中間值依次傳遞給下一個 reducer:
// 與上述手動編寫的 `rootReducer` 一樣
const rootReducer = reduceReducers(combinedReducer, crossSliceReducer);
應注意,使用 reduceReducers
時須確保第一個 reducer 能夠定義初始的 state 數據,因為後續的 reducers 通常會假定 state 樹已存在,也就不會為此提供默認狀態。
reduceReducers
與 combineReducers
之區別combineReducers
創建的是嵌套的狀態,每個 reducer 單獨負責其 state 塊(如 state.todos 與 state.users)reduceReducers
創建的是扁平的狀態,每個 reducer 都負責同一 state,尤適用於把數個操作同一 state 的 reducers 給 chain 起來。考慮以下簡單的例子:
// 此 reducer 為 state.sum 加上一 payload,
// 並紀錄操作總次數
function reducerAdd(state, payload) {
if (!state) state = { sum: 0, totalOperations: 0 };
if (!payload) return state;
return {
...state,
sum: state.sum + payload,
totalOperations: state.totalOperations + 1
};
}
// 此 reducer 為 state.product 乘以一 payload,
// 並紀錄操作總次數
function reducerMult(state, payload) {
if (!state) state = { product: 1, totalOperations: 0 };
if (!payload) return state;
// 因為 `reduceReducers` 本身之隱患,
// `product` 可以是 undefined(下文會述及)
const prev = state.product || 1;
return {
...state,
product: prev * payload,
totalOperations: state.totalOperations + 1
};
}
combineReducers
每個 reducer 均得到一獨立的 state 塊(見 文檔):
const rootReducer = combineReducers({
add: reducerAdd,
mult: reducerMult
});
const initialState = rootReducer(undefined);
/*
* {
* add: { sum: 0, totalOperations: 0 },
* mult: { product: 1, totalOperations: 0 },
* }
*/
const first = rootReducer(initialState, 4);
/*
* {
* add: { sum: 4, totalOperations: 1 },
* mult: { product: 4, totalOperations: 1 },
* }
*/
const second = rootReducer(first, 4);
/*
* {
* add: { sum: 8, totalOperations: 2 },
* mult: { product: 16, totalOperations: 2 },
* }
*/
reduceReducers
所有 reducers 都共享同一 state:
const addAndMult = reduceReducers(reducerAdd, reducerMult);
const initial = addAndMult(undefined);
/*
* {
* sum: 0,
* totalOperations: 0
* }
*
* 首先調用 `reducerAdd`,得到初始狀態 { sum: 0 }
* 然後調用 `reducerMult`,但沒有 payload,
* 故祇得到不變的 state,
* 而不見 `product` 屬性
*/
const next = addAndMult(initial, 4);
/*
* {
* sum: 4,
* product: 4,
* totalOperations: 2
* }
*
* 首先調用 `reducerAdd`,得到 `sum` = 0 + 4 = 4
* 然後調用 `reducerMult`,得到 `product` = 1 * 4 = 4
* 二 reducers 都改變 `totalOperations`
*/
const final = addAndMult(next, 4);
/*
* {
* sum: 8,
* product: 16,
* totalOperations: 4
* }
*/
完 :)