【问题标题】:How to reuse reducers in redux toolkit with typescript?如何在带有 typescript 的 redux 工具包中重用 reducer?
【发布时间】:2021-06-30 19:08:49
【问题描述】:

我有一个这样的切片:

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 开头的部分编写一段可重用的代码,因为我将在多个切片中处理相同的异步请求。我阅读了文档,但仍然无法理解如何完成此操作。

【问题讨论】:

    标签: reactjs typescript react-redux redux-toolkit


    【解决方案1】:
    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,否则它将无法正常工作。

    编辑

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

    【讨论】:

    • 感谢您的回答。仍然没有尝试过另一种方式。还想问,如何在不创建另一个构建器的情况下从同一个切片添加更多异步 thunk(如 getUser)?
    • github.com/reduxjs/redux-toolkit/issues/429 -> 这可能对你有帮助。尤其是使用isAnyOf 查看倒数第二条评论。 builder.addMatcher(isAnyOf (tunkOne, thunkTwo), (state, action) =&gt; ...
    【解决方案2】:

    有一个example in the docs,它展示了如何使用addMatcheraction.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')
    }
    

    动作匹配器是一个柯里化函数,它接受一对多的 thunk,如果动作名称以任何这些 thunk 的 typePrefix 开头,则返回 true

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

    case reducer 处理 thunk(s) 的所有三种情况。它调用类型保护函数来确定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

    【讨论】:

    • 刚刚尝试了这个解决方案,但由于某种原因打字稿给了我这个错误:第 11:51 行:解析错误:意外的令牌,预期的“]”指向类型 PendingAction = ReturnType 值得一提的是,我使用的是默认的 CRA tsconfig。
    猜你喜欢
    • 1970-01-01
    • 2021-09-15
    • 1970-01-01
    • 2021-06-02
    • 2022-01-22
    • 2021-11-07
    • 2016-05-30
    • 2022-10-02
    • 2022-07-28
    相关资源
    最近更新 更多