likes
comments
collection
share

Redux Toolkit 可能是目前 Redux 的最佳实践

作者站长头像
站长
· 阅读数 13

关于「你不知道的 Redux」这个专栏

Redux 是一个比较好的前端进阶切入点,可以帮助我们更好地管理应用程序的状态,提高编程设计思想,同时也是面试中的高频考点。相比于 react 和 webpack 系列,Redux 学习成本较低,代码易读且代码量不大,但是同样具有丰富的生态。本专栏将持续更新 Redux 相关知识,包括 React-Redux、Redux 中间件、Immer 等等,甚至会考虑其他的解决方案,比如最近的 Zustand 等。通过学习本专栏,读者可以全面了解 Redux 生态,提高自己的技能水平,更好地应对实际项目中的需求。

Redux Toolkit 可能是目前 Redux 的最佳实践

本文主要介绍 Redux Toolkit 核心 API 的具体实现和应用,帮助大家更好地理解 RTK 各种 API 的 设计思想和原理。

Redux Toolkit 是什么?

Redux Toolkit本身不是 Redux 的替代品,而是 Redux 官方推荐的工具集,它包含了一系列的工具函数和 API,它可以帮助我们用更少的代码来实现 Redux 的功能。

为什么需要 Redux Toolkit?

Redux 的核心概念非常简单,但是在实际的项目中,Redux 的使用会变得复杂,不仅仅是因为 Redux 本身的复杂性,还因为 Redux 本身的 API 设计并不是很友好,需要我们编写大量的样板代码,比如在编写 action、reducer 时,我们需要编写大量的 switch...case 语句,还需要手动编写 action 的类型常量,这些都会增加我们的工作量。

Redux Toolkit(后续简称 RTK) 可以帮助我们解决这些问题,它提供了一系列的工具函数和 API,可以帮助我们简化 Redux 的使用,减少样板代码的编写,提高开发效率。

Redux Toolkit 重点解决和优化哪些问题?

  • "配置 Redux store 太复杂"
  • "我必须添加很多包才能使 Redux 有用"
  • "Redux 需要太多样板代码"

如何解决 Store 配置复杂的问题

传统 Redux Store 的配置较为复杂

import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

// 定义初始 state
const initialState = {};

// 定义 middleware,包括 thunk 和用于 HMR 的 module.hot.accept middleware
const middleware = [thunk];
if (process.env.NODE_ENV === "development") {
  middleware.push(require("redux-logger").createLogger());

  // 添加用于 HMR 的 module.hot.accept middleware
  const { logger } = require("redux-logger");
  const { composeWithDevTools } = require("redux-devtools-extension");
  const { AppContainer } = require("react-hot-loader");
  const appReducer = rootReducer;
  const composeEnhancers = composeWithDevTools({});

  middleware.push((store) => (next) => (action) => {
    const returnValue = next(action);
    if (module.hot) {
      module.hot.accept("./reducers", () => {
        const nextReducer = require("./reducers").default;
        store.replaceReducer(nextReducer);
      });
    }
    return returnValue;
  });
}

// 创建 store
const store = createStore(
  rootReducer,
  initialState,
  compose(
    applyMiddleware(...middleware),
    process.env.NODE_ENV === "development" ? composeEnhancers() : (f) => f
  )
);

RTK 提供了configureStore(),封装了很多配置细节

configureStore() 函数用于创建 Redux store,它接收一个配置对象作为参数,配置对象有以下属性。

  • reducer:一个函数或一个对象,用于指定 Redux store 的 reducer。
  • middleware:一个数组,用于指定 Redux store 的中间件。
  • devTools:一个布尔值,用于指定是否启用 Redux DevTools。
  • preloadedState:一个对象,用于指定 Redux store 的初始状态。
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";

const store = configureStore({
  reducer: rootReducer,
  middleware: [],
  devTools: true,
  preloadedState: {},
});

如何解决 Redux 完整解决方案,依赖包过多的问题

传统 Redux 需要安装很多依赖包

npm install redux react-redux redux-thunk redux-logger redux-devtools-extension redux-saga immer

RTK 只需要安装一个依赖包, 并内置了常用的 package

npm install @reduxjs/toolkit

重点提一下内置的 immer,它可以帮助我们更方便地更新 state,而不需要手动编写不可变更新的代码,在 Redux 官方文档中也被强烈推荐使用。

如何解决 Redux 需要编写大量样板代码的问题

传统 Redux 需要编写大量的样板代码

// 定义 action 的类型常量
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

// 定义 action
export const increment = (payload) => ({
  type: INCREMENT,
  payload,
});

export const decrement = (payload) => ({
  type: DECREMENT,
  payload,
});

RTK 提供了createAction,可以帮助我们更方便地编写 action 和 action Creator

import { createAction } from "@reduxjs/toolkit";

export const increment = createAction("counter/increment");
export const decrement = createAction("counter/decrement");

RTK createAction 源码实现

// 类似函数式编程中高阶函数的概念,createAction 函数接收一个 type 参数,返回一个 actionCreator 函数
export function createAction(type, prepareAction) {
  const actionCreator = (...args) => {
    if (prepareAction) {
      // 如果传入了 prepareAction 参数,则调用 prepareAction 函数,将返回的对象作为 action 的 payload,常用于对 action 的 payload 做进一步处理
      let prepared = prepareAction(...args);
      if (!prepared) {
        throw new Error(
          "Did you forget to return an object from the prepare callback?"
        );
      }
      return { type, payload: prepared.payload, meta: prepared.meta };
    }
    return { type, payload: args[0] }; // 如果没有传入 prepareAction 参数,则直接将第一个参数作为 action 的 payload
  };
  actionCreator.toString = () => `${type}`;
  actionCreator.type = type; // 为 actionCreator 函数添加 type 属性,用于判断 actionCreator 的类型
  actionCreator.match = (action) => action.type === type; // 为 actionCreator 函数添加 match 方法,兼容后续的 createReducer 中的 MatchReducer
  return actionCreator;
}

RTK createReducer 可以帮助我们更方便地编写 reducer,并增加了 MatcherReducer 的能力

官方支持 MapObject 和 Builder Callback 的方式声明 reducer,这里使用后者

const initialState: Record<string, string> = {};
const resetAction = createAction("reset-tracked-loading-state");

function isPendingAction(action: AnyAction): action is PendingAction {
  return action.type.endsWith("/pending");
}

const reducer = createReducer(initialState, (builder) => {
  builder
    .addCase(resetAction, () => initialState)
    // matcher can be defined outside as a type predicate function
    .addMatcher(isPendingAction, (state, action) => {
      state[action.meta.requestId] = "pending";
    })
    .addDefaultCase((state, action) => {
      state.otherActions++;
    });
});

RTK createReducer 源码实现

export function createReducer<S extends NotFunction<any>>(
  initialState: S | (() => S),
  mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void,
  actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
  defaultCaseReducer?: CaseReducer<S>
): ReducerWithInitialState<S> {
  let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
    typeof mapOrBuilderCallback === "function"
      ? executeReducerBuilderCallback(mapOrBuilderCallback) // 实现并不复杂,就是将传入的 mapOrBuilderCallback 中 builder注册的case, Match,defaultCase 分别处理,得到 actionsMap、finalActionMatchers、finalDefaultCaseReducer 三个变量
      : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer];

  function reducer(state = initialState(), action: any): S {
    let caseReducers = [
      actionsMap[action.type],
      ...finalActionMatchers
        .filter(({ matcher }) => matcher(action))
        .map(({ reducer }) => reducer),
    ];
    // 将 actionsMap 中匹配到的 caseReducer 和 finalActionMatchers 中匹配到的 caseReducer 放到 caseReducers 数组中
    // 最终得到的是匹配成功的 reducer 数组
    if (caseReducers.filter((cr) => !!cr).length === 0) {
      caseReducers = [finalDefaultCaseReducer];
    }
    // 对匹配到的 reducer 数组依次执行,返回最终的 state
    // reducer 为什么取名 reducer 的原因之一
    return caseReducers.reduce((previousState, caseReducer): S => {
      if (caseReducer) {
        if (isDraft(previousState)) {
          // If it's already a draft, we must already be inside a `createNextState` call,
          // likely because this is being wrapped in `createReducer`, `createSlice`, or nested
          // inside an existing draft. It's safe to just pass the draft to the mutator.
          const draft = previousState as Draft<S>; // We can assume this is already a draft
          const result = caseReducer(draft, action);

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

          return result as S;
        } else if (!isDraftable(previousState)) {
          // If state is not draftable (ex: a primitive, such as 0), we want to directly
          // return the caseReducer func and not wrap it with produce.
          const result = caseReducer(previousState as any, action);

          return result as S;
        } else {
          return createNextState(previousState, (draft: Draft<S>) => {
            return caseReducer(draft, action);
          });
        }
      }

      return previousState;
    }, state);
  }

  reducer.getInitialState = getInitialState;
  // 还是 高阶函数思想,传入一个 reducer builder ,返回 reducer 函数, 等到reducer函数执行时,最终返回 state
  return reducer as ReducerWithInitialState<S>;
}

executeReducerBuilderCallback 源码实现

export function executeReducerBuilderCallback<S>(
  builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): [
  CaseReducers<S, any>,
  ActionMatcherDescriptionCollection<S>,
  CaseReducer<S, AnyAction> | undefined
] {
  const actionsMap: CaseReducers<S, any> = {};
  const actionMatchers: ActionMatcherDescriptionCollection<S> = [];
  let defaultCaseReducer: CaseReducer<S, AnyAction> | undefined;
  const builder = {
    addCase(
      typeOrActionCreator: string | TypedActionCreator<any>,
      reducer: CaseReducer<S>
    ) {
      const type =
        typeof typeOrActionCreator === "string"
          ? typeOrActionCreator
          : typeOrActionCreator.type;
      actionsMap[type] = reducer;
      return builder;
    },
    addMatcher<A>(
      matcher: TypeGuard<A>,
      reducer: CaseReducer<S, A extends AnyAction ? A : A & AnyAction>
    ) {
      actionMatchers.push({ matcher, reducer });
      return builder;
    },
    addDefaultCase(reducer: CaseReducer<S, AnyAction>) {
      defaultCaseReducer = reducer;
      return builder;
    },
  };
  builderCallback(builder);
  return [actionsMap, actionMatchers, defaultCaseReducer];
}

RTK createSlice 整合 createReducercreateAction

提供将 state 按照 业务实体拆分的功能,同时提供了 action 和 reducer 的声明方式,减少了 action 和 reducer 的声明代码

const initialState = { value: 0 };

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    incremented(state) {
      state.value++;
    },
    decremented(state) {
      state.value--;
    },
    incrementedBy(state, action: PayloadAction<number>) {
      state.value += action.payload;
    },
  },
  // 同时支持 builder callback 和 map object模式
  extraReducers: (builder) => {
    builder.addCase(incremented, (state) => {
      state.value++;
    });
    builder.addMatcher(isAnyOf(incremented, decremented), (state) => {
      state.value++;
    });
  },
});
// 不再需要重复声明 action 和 action Reducer ,直接在 UI层 dispatch action 即可
export const { incremented, decremented, incrementedBy } = counterSlice.actions;
// 符合 duck Redux 模式,直接导出 reducer
export default counterSlice.reducer;

RTK createSlice 源码实现

export function createSlice<
  State,
  CaseReducers extends SliceCaseReducers<State>,
  Name extends string = string
>(
  options: CreateSliceOptions<State, CaseReducers, Name>
): Slice<State, CaseReducers, Name> {
  const { name } = options;

  const initialState =
    typeof options.initialState == "function"
      ? options.initialState
      : freezeDraftable(options.initialState);

  const reducers = options.reducers || {};

  const reducerNames = Object.keys(reducers);

  const sliceCaseReducersByName: Record<string, CaseReducer> = {};
  const sliceCaseReducersByType: Record<string, CaseReducer> = {};
  const actionCreators: Record<string, Function> = {};

  reducerNames.forEach((reducerName) => {
    const maybeReducerWithPrepare = reducers[reducerName];
    const type = getType(name, reducerName);

    let caseReducer: CaseReducer<State, any>;
    let prepareCallback: PrepareAction<any> | undefined;

    if ("reducer" in maybeReducerWithPrepare) {
      caseReducer = maybeReducerWithPrepare.reducer;
      prepareCallback = maybeReducerWithPrepare.prepare;
    } else {
      caseReducer = maybeReducerWithPrepare;
    }

    sliceCaseReducersByName[reducerName] = caseReducer;
    sliceCaseReducersByType[type] = caseReducer;
    // 遍历 slice.reducer,帮助我们自动生成对应的 actionCreator,达到减负的目的
    actionCreators[reducerName] = prepareCallback
      ? createAction(type, prepareCallback)
      : createAction(type);
  });

  function buildReducer() {
    const [
      extraReducers = {},
      actionMatchers = [],
      defaultCaseReducer = undefined,
    ] =
      typeof options.extraReducers === "function"
        ? // 这里的 extraReducers 和上文 createReducer 一样,返回 actionMap 和 actionMatcher
          executeReducerBuilderCallback(options.extraReducers)
        : [options.extraReducers];

    // 将 slice.reducer 和 extraReducers 合并
    const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType };

    // 掉用 createReducer 生成最终的 reducer
    return createReducer(initialState, (builder) => {
      for (let key in finalCaseReducers) {
        builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>);
      }
      for (let m of actionMatchers) {
        builder.addMatcher(m.matcher, m.reducer);
      }
      if (defaultCaseReducer) {
        builder.addDefaultCase(defaultCaseReducer);
      }
    });
  }

  let _reducer: ReducerWithInitialState<State>;

  return {
    name,
    reducer(state, action) {
      if (!_reducer) _reducer = buildReducer();

      return _reducer(state, action);
    },
    //注意这里按照上述代码只会暴露 reducer中声明的 actionCreator,不会b暴露 extraReducers 中的 actionCreator
    actions: actionCreators as any,
    caseReducers: sliceCaseReducersByName as any,
    getInitialState() {
      if (!_reducer) _reducer = buildReducer();

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

RTK createAsyncThunk 异步 action

使用 redux-thunk 作为异步中间件, 通过 createAsyncThunk 创建异步 action,可以在 extraReducers 中注册异步 action 的 pending, fulfilled, rejected 三个状态对应的 reducer 方法

// 1. 定义异步 action
const fetchUserById = createAsyncThunk(
  "users/fetchByIdStatus",
  async (userId: number) => {
    const response = await userAPI.fetchById(userId);
    return response.data;
  }
);
// 2. 定义 reducer
const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: (builder) => {
    // 可以在extraReducers中,注册异步 action 的 pending, fulfilled, rejected 三个状态对应的reducer方法
    builder.addCase(fetchUserById.pending, (state, action) => {
      // Add user to the state array
    });
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      // Add user to the state array
    });
    builder.addCase(fetchUserById.rejected, (state, action) => {
      // Add user to the state array
    });
  },
});

RTK createAsyncThunk 源码实现

export const createAsyncThunk = (() => {
  function createAsyncThunk(
    typePrefix: string,
    payloadCreator,
    options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
  ) {
    const fulfilled: AsyncThunkFulfilledActionCreator<
      Returned,
      ThunkArg,
      ThunkApiConfig
    > = createAction(
      typePrefix + "/fulfilled",
      (
        payload: Returned,
        requestId: string,
        arg: ThunkArg,
        meta?: FulfilledMeta
      ) => ({
        payload,
        meta: {
          ...((meta as any) || {}),
          arg,
          requestId,
          requestStatus: "fulfilled" as const,
        },
      })
    );
    // 在 asyncThunk中 会帮助我们提供 pending, fulfilled, rejected 三个状态对应的 actionCreator
    const pending: AsyncThunkPendingActionCreator<ThunkArg, ThunkApiConfig> =
      createAction(
        typePrefix + "/pending",
        (requestId: string, arg: ThunkArg, meta?: PendingMeta) => ({
          payload: undefined,
          meta: {
            ...((meta as any) || {}),
            arg,
            requestId,
            requestStatus: "pending" as const,
          },
        })
      );

    const rejected: AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig> =
      createAction(
        typePrefix + "/rejected",
        (
          error: Error | null,
          requestId: string,
          arg: ThunkArg,
          payload?: RejectedValue,
          meta?: RejectedMeta
        ) => ({
          payload,
          error: ((options && options.serializeError) || miniSerializeError)(
            error || "Rejected"
          ) as GetSerializedErrorType<ThunkApiConfig>,
          meta: {
            ...((meta as any) || {}),
            arg,
            requestId,
            rejectedWithValue: !!payload,
            requestStatus: "rejected" as const,
            aborted: error?.name === "AbortError",
            condition: error?.name === "ConditionError",
          },
        })
      );

    let displayedWarning = false;
    // 在 thunk方法中,最终返回一个 actionCreator,被dispatch掉用后,会执行这个 actionCreator
    function actionCreator(
      arg: ThunkArg
    ): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
      return (dispatch, getState, extra) => {
        const requestId = options?.idGenerator
          ? options.idGenerator(arg)
          : nanoid();

        const abortController = new AC();
        let abortReason: string | undefined;

        let started = false;
        function abort(reason?: string) {
          abortReason = reason;
          abortController.abort();
        }
        // actionCreator 中最终会返回一个 promise
        const promise = (async function () {
          let finalAction: ReturnType<typeof fulfilled | typeof rejected>;
          try {
            let conditionResult = options?.condition?.(arg, {
              getState,
              extra,
            });
            if (isThenable(conditionResult)) {
              conditionResult = await conditionResult;
            }
            started = true;

            const abortedPromise = new Promise<never>((_, reject) =>
              abortController.signal.addEventListener("abort", () =>
                reject({
                  name: "AbortError",
                  message: abortReason || "Aborted",
                })
              )
            );
            // 首先会触发 pending action
            dispatch(
              pending(
                requestId,
                arg,
                options?.getPendingMeta?.(
                  { requestId, arg },
                  { getState, extra }
                )
              )
            );
            // finalAction 其实是 payloadCreator  和 aborted(RTK中支持取消 async action)的竞态promise
            finalAction = await Promise.race([
              abortedPromise,
              Promise.resolve(
                payloadCreator(arg, {
                  dispatch,
                  getState,
                  extra,
                  requestId,
                  signal: abortController.signal,
                  abort,
                  rejectWithValue: ((
                    value: RejectedValue,
                    meta?: RejectedMeta
                  ) => {
                    return new RejectWithValue(value, meta);
                  }) as any,
                  fulfillWithValue: ((value: unknown, meta?: FulfilledMeta) => {
                    return new FulfillWithMeta(value, meta);
                  }) as any,
                })
              ).then((result) => {
                if (result instanceof RejectWithValue) {
                  throw result;
                }
                if (result instanceof FulfillWithMeta) {
                  return fulfilled(result.payload, requestId, arg, result.meta);
                }
                return fulfilled(result as any, requestId, arg);
              }),
            ]);
          } catch (err) {
            finalAction =
              err instanceof RejectWithValue
                ? rejected(null, requestId, arg, err.payload, err.meta)
                : rejected(err as any, requestId, arg);
          }

          const skipDispatch =
            options &&
            !options.dispatchConditionRejection &&
            rejected.match(finalAction) &&
            (finalAction as any).meta.condition;

          if (!skipDispatch) {
            dispatch(finalAction);
          }
          return finalAction;
        })();
        return Object.assign(promise as Promise<any>, {
          abort,
          requestId,
          arg,
          unwrap() {
            return promise.then<any>(unwrapResult);
          },
        });
      };
    }

    return Object.assign(actionCreator, {
      pending,
      rejected,
      fulfilled,
      typePrefix,
    });
  }
  createAsyncThunk.withTypes = () => createAsyncThunk;

  return createAsyncThunk as CreateAsyncThunk<AsyncThunkConfig>;
})();

整体而言,RTK 的优点

RTK 提供了 createAction ,createReducer 帮助我们去减少模版代码的同时,增强了 reducer 的 Match 能力,但是 RTK 并不止步于此,继续提供了 createSlice 帮助我们进一步简化代码,大大提升了开发体验,同时提供了 createAsyncThunk 帮助我们去处理异步的 action

关于 Redux 异步中间件的选择

社区可选方案比较多,比如 redux-thunk, redux-saga, redux-observable, redux-promise 等等。 在实际使用中,我们发现 redux-thunk 能够覆盖大多数应用场景,redux 官方也是建议优先使用 redux-thunk, 在有需要不能满足的情况再集成 redux-saga 等方案。即在 rtk 中是可以同时支持多种异步中间件

转载自:https://juejin.cn/post/7239187734417244216
评论
请登录