iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0

Redux Toolkit 相關套件

必安裝套件:

  • redux-toolkit : 簡化 Redux 的使用,讓開發者能更輕鬆地管理狀態和減少樣板程式碼,並且處理相關套件。
  • react-redux : 將 React 和 Redux 結合起來,使用 Provider 可以使 Redux store 可用於整個應用,並使用 useSelector 和 useDispatch 等 Hook 來讀取狀態和發送動作。

相關套件

  • redux : Redux Toolkit 的運作是建立在 Redux 的基本架構之上的

  • immer.js : Redux Toolkit 在內部利用 Immer 來簡化狀態更新的邏輯,讓 Reducers 操作狀態變得更直觀。

在此文章中,只會簡單介紹這些概念,不會詳細說明如何使用,具體用法請參考官方文件

Redux Toolkit 基本概念

  • Store:用來存放資料狀態。
  • Provider:由 react-redux 提供,在所有組件的最外面包一層 Provider,傳入 store,所有被包覆的組件都可以使用到 store 的狀態。
  • Slice:每個 slice 包含了一部分的 state、對應的 reducer 和 action。
  • Reducer:可以視為事件的處理函數,它根據接收到的 action 類型來更新 state。當某個 action 被 dispatch 時,對應的 reducer 會被調用,根據當前的 state 和 action 返回新的 state。
  • Action:是描述要發生什麼事件的物件,reducer 依據這些 action 來判斷如何更新 state。透過 dispatch 方法來發送 action,從而觸發對應的 reducer。

Redux Toolkit 原始碼解析

以下的內容將會進行簡化處理,不會完整展示每一個細節,並且會去掉 type 定義以便於閱讀。如果有興趣深入了解,可以參考 原始碼,建議搭配實際的使用案例來更好地理解。

CreateAction

將 type 轉換成 { type, payload: ... } 的格式

function createAction(type, prepareAction) {
  function actionCreator(...args) {
    //如果有 prepareAction,處理 payload
    if (prepareAction) {
      const prepared = prepareAction(...args);
      if (!prepared) {
        throw new Error("prepareAction did not return an object");
      }
      return {
        type,
        payload: prepared.payload,
        meta: prepared.meta,
        error: prepared.error,
      };
    }
    //如果沒有 prepareAction,傳入的第一個參數作為 payload
    return { type, payload: args[0] };
  }

  actionCreator.toString = () => ${type};
  actionCreator.type = type;
  //比對傳入的 action 是否屬於此類型
  actionCreator.match = (action) => action.type === type;

  return actionCreator;
}

CreateReducer

export function createReducer(
  initialState,
  mapOrBuilderCallback,
  actionMatchers = [],
  defaultCaseReducer
) {
  // 針對不同的參數情況做處理
  let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
    typeof mapOrBuilderCallback === "function"
      ? executeReducerBuilderCallback(mapOrBuilderCallback)
      : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer];

  // 確保初始狀態是不可變的,透過 immer 提供的 createNextState
  let getInitialState;
  if (isStateFunction(initialState)) {
    getInitialState = () => createNextState(initialState(), () => {});
  } else {
    const frozenInitialState = createNextState(initialState, () => {});
    getInitialState = () => frozenInitialState;
  }

  function reducer(state = getInitialState(), action) {
    //根據 action 的類型從 actionsMap 獲取對應的 case reducer
    let caseReducers = [
      actionsMap[action.type],
      ...finalActionMatchers
        .filter(({ matcher }) => matcher(action))
        .map(({ reducer }) => reducer),
    ];
    //如果沒有,則使用默認 case reducer
    if (caseReducers.filter((cr) => !!cr).length === 0) {
      caseReducers = [finalDefaultCaseReducer];
    }

    return caseReducers.reduce((previousState, caseReducer) => {
      if (caseReducer) {
        // isDraft是 immer 提供的function,用來確認是由 immer 產生的可變狀態
        if (isDraft(previousState)) {
          const draft = previousState;
          const result = caseReducer(draft, action);

          if (typeof result === "undefined") {
            return previousState;
          }

          return result;
        } else if (!isDraftable(previousState)) {
          // 無法由immer代理,像是 primitive type,則直接使用原始的 previousState
          const result = caseReducer(previousState, action);
          //針對 undefined 的情況做處理
          if (typeof result === "undefined") {
            // 如果 previousState 是 null,直接返回它是合理的
            if (previousState === null) {
              return previousState;
            }
            throw Error(
              "A case reducer on a non-draftable value must not return undefined"
            );
          }

          return result;
        } else {
          return createNextState(previousState, (draft) => {
            return caseReducer(draft, action);
          });
        }
      }

      return previousState;
    }, state);
  }

  reducer.getInitialState = getInitialState;
  //返回 reducer 並設置初始狀態函數
  return reducer;
}

CreateReducer 使用範例

const reducer = createReducer(
  {
    counter: 0,
    sumOfNumberPayloads: 0,
    unhandledActions: 0,
  },
  (builder) => {
    builder
      .addCase(increment, (state, action) => {
        state.counter += action.payload;
      })
      .addCase(decrement, (state, action) => {
        state.counter -= action.payload;
      })
      .addMatcher(isActionWithNumberPayload, (state, action) => {})
      .addDefaultCase((state, action) => {});
  }
);
const increment = createAction("increment");
const decrement = createAction("decrement");

const counterReducer = createReducer(0, {
  [increment]: (state, action) => state + action.payload,
  [decrement]: (state, action) => state - action.payload,
});

CreateSlice

function createSlice(options) {
  // options裡面有包含 name、initialState、reducers
  const { name } = options;

  if (!name) {
    throw new Error("`name` is a required option for createSlice");
  }

  const initialState =
    typeof options.initialState == "function"
      ? options.initialState
      : createNextState(options.initialState, () => {});
  // createNextState是 immer 提供的方法 ,使用 createNextState 包裝,確保可以進行 immutable 的 state 操作

  const reducers = options.reducers || {};

  const reducerNames = Object.keys(reducers);

  const sliceCaseReducersByName = {};
  const sliceCaseReducersByType = {};
  const actionCreators = {};

  reducerNames.forEach((reducerName) => {
    const maybeReducerWithPrepare = reducers[reducerName];
    // 生成 action type,會是 `${name}/${reducerName}`
    const type = getType(name, reducerName);

    let caseReducer;
    let prepareCallback;

    // 如果是物件且包含reducer,那結構會是 { reducer, prepare }
    if ("reducer" in maybeReducerWithPrepare) {
      caseReducer = maybeReducerWithPrepare.reducer;
      prepareCallback = maybeReducerWithPrepare.prepare;
    } else {
      caseReducer = maybeReducerWithPrepare;
    }

    sliceCaseReducersByName[reducerName] = caseReducer;
    sliceCaseReducersByType[type] = caseReducer;
    // 如果有prepareCallback,則會處理payload
    // 可參考上方createAction
    actionCreators[reducerName] = prepareCallback
      ? createAction(type, prepareCallback)
      : createAction(type);
  });

  // 生成reducer
  function buildReducer() {
    const [
      extraReducers = {},
      actionMatchers = [],
      defaultCaseReducer = undefined,
    ] =
      typeof options.extraReducers === "function"
        ? executeReducerBuilderCallback(options.extraReducers)
        : [options.extraReducers];

    // 傳入extraReducers
    const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType };
    // 使用 createReducer 創建最終的 reducer,可參考上方的 createReducer
    return createReducer(
      initialState,
      finalCaseReducers,
      actionMatchers,
      defaultCaseReducer
    );
  }

  let _reducer; //最終的 reducer

  return {
    name,
    // 可以傳到configureStore
    reducer(state, action) {
      if (!_reducer) _reducer = buildReducer();

      return _reducer(state, action);
    },
    // 讓dispatch使用
    actions: actionCreators,
    caseReducers: sliceCaseReducersByName,
    getInitialState() {
      if (!_reducer) _reducer = buildReducer();

      return _reducer.getInitialState();
    },
  };
}

configureStore

configureStore(options) {
  // 設置默認的middleware
  const curriedGetDefaultMiddleware = curryGetDefaultMiddleware()

  const {
    reducer = undefined,
    middleware = curriedGetDefaultMiddleware(),
    devTools = true,
    preloadedState = undefined,
    enhancers = undefined,
  } = options || {}

  let rootReducer

  //如果 reducer 是一個函數,直接使用該函數。
  //如果是物件,則使用 combineReducers 將多個 reducer 組合起來。
  if (typeof reducer === 'function') {
    rootReducer = reducer
  } else if (isPlainObject(reducer)) {
    rootReducer = combineReducers(reducer)
  }

  // 設定 middleware
  let finalMiddleware = middleware
  if (typeof finalMiddleware === 'function') {
    finalMiddleware = finalMiddleware(curriedGetDefaultMiddleware)
  }

  const middlewareEnhancer = applyMiddleware(...finalMiddleware)

  let finalCompose = compose

  if (devTools) {
    // DevTools 相關設定
  }

  // 設定 enhancers 來增加 store
  let storeEnhancers = [middlewareEnhancer]
  if (Array.isArray(enhancers)) {
    storeEnhancers = [middlewareEnhancer, ...enhancers]
  } else if (typeof enhancers === 'function') {
    storeEnhancers = enhancers(storeEnhancers)
  }

  const composedEnhancer = finalCompose(...storeEnhancers)

  // 將參數傳入,使用 redux 的 createStore 建立最終的 store
  return createStore(rootReducer, preloadedState, composedEnhancer)
}

參考資料:
https://redux-toolkit.js.org/usage/usage-guide
https://redux.js.org/tutorials/essentials/part-1-overview-concepts
https://redux-toolkit.js.org/usage/immer-reducers
https://github.com/reduxjs/redux-toolkit/tree/fc5cf086c74cd4a5a2dbe81070febe9c09e2841d/packages/toolkit/src
https://juejin.cn/post/7058923547464466440
https://immerjs.github.io/immer/api/


上一篇
Day 09 - 掌握 Client State:React 狀態管理的核心概念
下一篇
Day 11 - Zustand 原理解析
系列文
前進React 生態系 : 技術應用與概念解析30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言