如何使用 typescript 在 redux 工具包中重用 reducer?

How to reuse reducers in redux toolkit with typescript?

我有这样的一片:

const authSlice = createSlice({
  name: "auth",
  initialState: {
    ...initialUserInfo,
    ...initialBasicAsyncState,
  },
  reducers: {
    setUser: (state, { payload }: PayloadAction<{ userObj: User }>) => {
      const { id, email, googleId, facebookId } = payload.userObj;
      state.id = id;
      state.email = email;
      if (googleId) state.googleId = googleId;
      if (facebookId) state.facebookId = facebookId;
    },
    clearUser: (state) => {
      state.id = "";
      state.email = "";
      state.googleId = "";
      state.facebookId = "";
    },
  },
  extraReducers: (builder) =>
    builder
      .addCase(getUser.pending, (state, action) => {
        state.isLoading = true;
      })
      .addCase(getUser.fulfilled, (state, action) => {
        state.isLoading = false;
        state.isSuccess = true;
        state.errors = null;
      })
      .addCase(getUser.rejected, (state, action) => {
        state.isLoading = false;
        if (action.payload) {
          state.errors = action.payload;
        } else if (action.error) state.errors = action.error;
      }),
});

我想为从 extraReducers 开始的部分编写一段可重用的代码,因为我将在多个片段中处理相同的异步请求。我阅读了文档,但仍然无法理解如何完成此操作。

interface CustomState {
  isLoading: boolean
  isSuccess: boolean
  errors: ValidationErrors | SerializedError | null
}

function apiReducerBuilder<T, U>(
  builder: ActionReducerMapBuilder<CustomState>,
  customThunk: AsyncThunk<
    T,
    U,
    {
      rejectValue: ValidationErrors
    }
  >
) {
  return builder
    .addCase(customThunk.pending, (state) => {
      state.isLoading = true
    })
    .addCase(customThunk.fulfilled, (state) => {
      state.isLoading = false
      state.isSuccess = true
      state.errors = null
    })
    .addCase(customThunk.rejected, (state, action) => {
      state.isLoading = false
      if (action.payload) {
        state.errors = action.payload
      } else if (action.error) state.errors = action.error
    })
}

用法:

extraReducers: (builder) => apiReducerBuilder(builder, getUser)

这仅在您传递相同的状态类型时才有效Custom State。而且您的打字非常适合thunk。否则你必须在调用 apiReducerBuilder

时声明类型

因此,请确保您始终将相同的 Custom State 传递给您的构建器和 Thunk,否则它将无法工作。

编辑

也许,您可以使用自定义状态类型扩展您的切片状态类型,它可能会起作用。我没试过。所以,我会留给你看看它是否有效。

有一个 example in the docs 展示了如何使用 addMatcher 和类型谓词,如 action.type.endsWith('/pending') 来匹配任何待处理的操作。

我玩了一下,其中一个困难的事情是需要按特定顺序调用 builder 函数:addCase,然后是 addMatcher ,然后 addDefaultCase。所以我们不能先应用一堆addMatcher调用,然后再正常使用builder

另一个困难的部分是了解 rejectWithValuepayload 类型,因为拒绝值不是 AsyncThunk 类型的泛型之一(至少不是直接的)。

我想到的最佳解决方案是使用单个 addMatcher 调用来处理所有三种 thunk 情况。


这些基本类型和类型保护是copied from the docs

type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>

type PendingAction = ReturnType<GenericAsyncThunk['pending']>
type RejectedAction = ReturnType<GenericAsyncThunk['rejected']>
type FulfilledAction = ReturnType<GenericAsyncThunk['fulfilled']>

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

function isRejectedAction(action: AnyAction): action is RejectedAction {
  return action.type.endsWith('/rejected')
}

function isFulfilledAction(action: AnyAction): action is FulfilledAction {
  return action.type.endsWith('/fulfilled')
}

动作匹配器是一个 curried 函数,如果动作名称以任何这些 thunk 的 typePrefix 开头,它接受一对多 thunk 和 returns true

const isThunk = <T extends AsyncThunk<any, any, any>[]>(...thunks: T) =>
  (action: AnyAction) => 
     thunks.some((thunk) => action.type.startsWith(thunk.typePrefix));

case reducer 处理 thunk 的所有三种情况。它调用类型保护函数来确定 action 是哪种类型并相应地更新状态。我们要求 state 使用我们正在更新的属性扩展类型。

type BasicAsyncState = {
  isLoading: boolean;
  isSuccess: boolean;
  errors: any;
};

const thunkHandler = <S extends BasicAsyncState>(
  state: Draft<S>,
  action: AnyAction
): void => {
  if (isPendingAction(action)) {
    state.isLoading = true;
  } else if (isFulfilledAction(action)) {
    state.isLoading = false;
    state.isSuccess = true;
    state.errors = null;
  } else if (isRejectedAction(action)) {
    state.isSuccess = false;
    state.isLoading = false;
    state.errors = action.error;
  }
};

您可以在与其他构建器回调相同的块中使用这两个函数。请记住 addMatcher 总是需要在 addCase 之后。

// can match one thunk
.addMatcher(isThunk(getUser), thunkHandler)
// or multiple thunks
.addMatcher(isThunk(getUser, loadSomething), thunkHandler)

Typescript Playground Link