将具有完全相同代码的 extraReducers 与 redux 工具包结合起来

combine extraReducers with exact same code with redux toolkit

我使用 redux 工具包创建了一个全局错误处理切片。我想重构它,让它更“干”:

const errorsSlice = createSlice({
  name: "error",
  initialState,
  reducers: {
    clearError: (state) => {
      state.errors = null;
      state.isOpen = false;
    },
  },
  extraReducers: {
    [createScript.rejected]: (state, { payload }) => {
      const errorArray = Object.values(payload.message).map(
        (key) => key.message
      );
      state.errors = errorArray;
      state.isOpen = true;
    },
    [createScene.rejected]: (state, { payload }) => {
      const errorArray = Object.values(payload.message).map(
        (key) => key.message
      );
      state.errors = errorArray;
      state.isOpen = true;
    },
  },
});

2 个 extraReducer 做完全相同的事情并且有效负载被归一化。我的代码可以正常工作。

有没有办法将 2“组合”到一个 extraReducer(最后会更多)?

我认为没有办法将两个 case reducer 组合成一个 case reducer,但您当然可以为每个提供相同的 reducer 功能。将重复的 reducer 函数重构为一个通用的 reducer 函数。

const rejectionReducer = (state, { payload }) => {
  const errorArray = Object.values(payload.message).map(
    (key) => key.message
  );
  state.errors = errorArray;
  state.isOpen = true;
};

...

const errorsSlice = createSlice({
  name: "error",
  initialState,
  reducers: {
    clearError: (state) => {
      state.errors = null;
      state.isOpen = false;
    },
  },
  extraReducers: {
    [createScript.rejected]: rejectionReducer,
    [createScene.rejected]: rejectionReducer,
});

更新

使用 isRejectedWithValue 高阶函数,您可以将 thunk 动作组合到匹配器中。

import { isRejectedWithValue } from '@reduxjs/toolkit';

const rejectionReducer = (state, { payload }) => {
  const errorArray = Object.values(payload.message).map(
    (key) => key.message
  );
  state.errors = errorArray;
  state.isOpen = true;
};

const errorsSlice = createSlice({
  name: "error",
  initialState,
  reducers: {
    clearError: (state) => {
      state.errors = null;
      state.isOpen = false;
    },
  },
  extraReducers: builder => {
    builder.addMatcher(
      isRejectedWithValue(createScript, createScene), // <-- thunk actions
      rejectionReducer
    );
  },
});

您目前正在用 "Map Object" notation. You want to use "Builder Callback" notation 定义您的 extraReducers

使用构建器回调,您可以使用 .addCase() 匹配单个操作,但您也可以使用 .addMatcher() 处理多个操作。 addMatcher() 的第一个参数是一个函数,它接受 action 和 returns 和 boolean 是否匹配。这里我们要匹配所有以'/rejected'.

结尾的动作
const errorsSlice = createSlice({
  name: "error",
  initialState,
  reducers: {
    clearError: (state) => {
      state.errors = null;
      state.isOpen = false;
    }
  },
  extraReducers: (builder) =>
    builder.addMatcher(
      // matcher function
      (action) => action.type.endsWith("/rejected"),
      // case reducer
      (state, { payload }) => {
        const errorArray = Object.values(payload.message).map(
          (key) => key.message
        );
        state.errors = errorArray;
        state.isOpen = true;
      }
    )
});

你可以使用我的帮手:

import { AnyAction, AsyncThunk } from '@reduxjs/toolkit';

enum AsyncActionStatusesEnum {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected',
}

//eslint-disable-next-line
type GenericAsyncThunk = AsyncThunk<any, any, any>;
type PendingAction = ReturnType<GenericAsyncThunk[AsyncActionStatusesEnum.PENDING]>;
type FulfilledAction = ReturnType<GenericAsyncThunk[AsyncActionStatusesEnum.FULFILLED]>;
type RejectedAction = ReturnType<GenericAsyncThunk[AsyncActionStatusesEnum.REJECTED]>;

// gets a list of asynchronous actions and checks them for the status of at least one === 'pending'
export function isSomeAsyncActionsPending(matchedActionTypes: GenericAsyncThunk[]) {
  return (action: AnyAction): action is PendingAction =>
    matchedActionTypes
      .map(actionType => `${actionType.typePrefix}/${AsyncActionStatusesEnum.PENDING}`)
      .some(actionType => action.type.endsWith(actionType));
}

// gets a list of asynchronous actions and checks them for the status of at least one === 'fulfilled'
export function isSomeAsyncActionsFulfilled(matchedActionTypes: GenericAsyncThunk[]) {
  return (action: AnyAction): action is FulfilledAction =>
    matchedActionTypes
      .map(actionType => `${actionType.typePrefix}/${AsyncActionStatusesEnum.FULFILLED}`)
      .some(actionType => action.type.endsWith(actionType));
}

// gets a list of asynchronous actions and checks them for the status of at least one === 'rejected'
export function isSomeAsyncActionsRejected(matchedActionTypes: GenericAsyncThunk[]) {
  return (action: AnyAction): action is RejectedAction =>
    matchedActionTypes
      .map(actionType => `${actionType.typePrefix}/${AsyncActionStatusesEnum.REJECTED}`)
      .some(actionType => action.type.endsWith(actionType));
}

使用示例:

const fetchCoockBookList = createAsyncThunk(
  'dataSlice/fetchCookBookList',
  async (data: CoockBookParamsType) => await getCoockBook(data)
);

const fetchSeriesList = createAsyncThunk(
  'dataSlice/fetchSeriesList',
  async (data: SeriesParamsType) => await getSeries(data)
);

const isActionsPending = isSomeAsyncActionsPending([
    fetchDataList,
    fetchSeriesList,
]);


const dataSlice = createSlice({
    // name, reducers, initialState, ...

    extraReducers: builder => {
    // others builder addCases, addMatchCases, ...

    builder
        .addMatcher(isActionsPending, state => {
            state.error = null;
            state.isLoading = true;
          })
        // addMatchers for fullfield, rejected
    }
})