所有文章

Redux Reducers 如何共享數據?

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;
  }
}

此中無深意,就不贅言了。

給 Action 添加額外數據

可以透過 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

新加一 Reducer 用以處理多塊數據交叉

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

已有一庫 reduce-reducers 可簡化上述流程(指 上一個解決方案),並把產生的中間值依次傳遞給下一個 reducer:

// 與上述手動編寫的 `rootReducer` 一樣
const rootReducer = reduceReducers(combinedReducer, crossSliceReducer);

應注意,使用 reduceReducers 時須確保第一個 reducer 能夠定義初始的 state 數據,因為後續的 reducers 通常會假定 state 樹已存在,也就不會為此提供默認狀態。

附:reduceReducerscombineReducers 之區別

  • 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
 * }
 */

完 :)

發佈於 8 Nov 2018

慎獨|純亦不已
Jason Lam on Twitter