如何重用 Redux Toolkit createSlice 函数中的 reducer 逻辑?

Posted

技术标签:

【中文标题】如何重用 Redux Toolkit createSlice 函数中的 reducer 逻辑?【英文标题】:How to reuse the reducer logic in Redux Toolkit createSlice function? 【发布时间】:2020-11-08 15:29:27 【问题描述】:

我是 React 新手,我正在学习使用 React 来构建 Web 应用程序。我发现 Redux Toolkit 很有用,并使用它的 createSlice() 函数来实现基本功能。但是,我遇到了一个与“最佳实践”相关的问题,我不确定我是否正确构建了应用程序的架构。

假设我有一个 user 对象存储在 Redux 中。我创建了一个异步 thunk 函数来获取相关信息:

export const getUserInfo = createAsyncThunk('user/get', async (userId, thunkApi) => 
    // fetching information using api

相应地,我处理pending/fulfilled/rejected回调如下:

const userSlice = createSlice(
    name: 'user',
    initialState,
    reducers: 
        setShowProgress(state, action: PayloadAction<boolean>) 
            state.showProgress = action.payload;
        ,
        clearError(state) 
            state.error = null;
            state.errorMessage = null;
        
    ,
    extraReducers: builder => 
        builder.addCase(getUserInfo.pending, (state, action) => 
            // My question is here >_<
        
        builder.addCase(getUserInfo.fulfilled, (state, action) => 
            // handle data assignments
        )
        builder.addCase(getUserInfo.rejected, (state, action) => 
            // handle error messages
        )
    
)

考虑到修改显示状态标志在其他功能 api 实现中很常见,我将这两个函数(setShowProgress()clearError())包装在 reducers 中。我的问题来了:如何引用getUserInfo.pending函数中的两个函数?

虽然我可以只在getUserInfo.pending 中分配showProgresserror 状态变量,而不是尝试调用reducer 函数,但是当我将来实现其他获取操作时肯定会引入重复代码。如果这不是推荐的模式,那么这种场景的最佳做法是什么?

【问题讨论】:

【参考方案1】:

如果您的目标只是基于pending/fulfilled/rejected 设置loading 布尔值或error 属性,您可以使用1.4 版中引入的addMatcher

这是一个非常基本的示例,它使用泛型助手在多个切片中简化此操作。

// First, we'll just create some helpers in the event you do this in other slices. I'd export these from a util.

const hasPrefix = (action: AnyAction, prefix: string) =>
  action.type.startsWith(prefix);
const isPending = (action: AnyAction) => action.type.endsWith("/pending");
const isFulfilled = (action: AnyAction) => action.type.endsWith("/fulfilled");
const isRejected = (action: AnyAction) => action.type.endsWith("/rejected");

const isPendingAction = (prefix: string) => (
  action: AnyAction
): action is AnyAction =>  // Note: this cast to AnyAction could also be `any` or whatever fits your case best
  return hasPrefix(action, prefix) && isPending(action);
;

const isRejectedAction = (prefix: string) => (
  action: AnyAction
): action is AnyAction =>  // Note: this cast to AnyAction could also be `any` or whatever fits your case best - like if you had standardized errors and used `rejectWithValue`
  return hasPrefix(action, prefix) && isRejected(action);
;

const isFulfilledAction = (prefix: string) => (
  action: AnyAction
): action is AnyAction => 
  return hasPrefix(action, prefix) && isFulfilled(action);
;

const userSlice = createSlice(
    name: 'user',
    initialState,
    reducers: ,
    extraReducers: builder => 
      builder.addCase(getUserInfo.fulfilled, (state, action) => 
          // handle data assignments
      )
      // use scoped matchers to handle generic loading / error setting behavior for async thunks this slice cares about
      .addMatcher(isPendingAction("user/"), state => 
        state.loading = true;
        state.error = '';
      )
      .addMatcher(isRejectedAction("user/"), (state, action) => 
        state.loading = false;
        state.error = action.error; // or you could use `rejectWithValue` and pull it from the payload.
      )
      .addMatcher(isFulfilledAction("user/"), state => 
        state.loading = false;
        state.error = '';
      );
    
)

【讨论】:

【参考方案2】:

我在使用 redux-toolkit 中的 createReducer 替换旧的 switch case 方法时遇到了类似的问题。

在旧的方法中,我经常为许多操作使用一个通用的 reducer,如下所示:

const authentication = (state = initialState, action: AuthenticationActions) => 
  switch (action.type) 
    case SIGN_UP_SUCCESS:
    case SIGN_IN_SUCCESS:
      return 
        ...state,
        account: action.payload,
      ;
    case SIGN_OUT_SUCCESS:
      return initialState;
    default:
      return state;
  
;

在新方法中,我必须创建两个相同的减速器并在.addCase 中使用它们。 所以我创建了一个通用助手来匹配多个动作:

import  Action, AnyAction  from '@reduxjs/toolkit';

declare interface TypedActionCreator<Type extends string> 
  (...args: any[]): Action<Type>;
  type: Type;

const isOneOf = <ActionCreator extends TypedActionCreator<string>>(actions: ActionCreator[]) => (
  (action: AnyAction): action is ReturnType<ActionCreator> => (
    actions.map(( type ) => type).includes(action.type)
  )
);

现在我们可以轻松地将它与.addMatcher 一起使用:

const authentication = createReducer(initialState, (builder) => (
  builder
    .addCase(signOutSuccess, () => (
      ...initialState,
    ))
    .addMatcher(isOneOf([signInSuccess, signUpSuccess]), (state, action) => (
      ...state,
      account: action.payload,
    ))
));

【讨论】:

以上是关于如何重用 Redux Toolkit createSlice 函数中的 reducer 逻辑?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Storybook 中使用 redux-toolkit?

如何在 Redux-toolkit 中使用两个不同的切片?

如何将 redux-toolkit createSlice 与 React 类组件一起使用

如何从 Redux Toolkit 中去抖 createAsyncThunk

使用 @reduxjs/toolkit 中的 configureStore 时如何重置 Redux Store 的状态?

如何实现redux-toolkit和next,js又不丢s-s-r