Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to reuse the reducer logic in Redux Toolkit createSlice function?

I'm new to React and I'm learning to use React to build a web app. I found Redux Toolkit useful and use its createSlice() function to implement the basic features. However, I encountered a "best practice"-related problem and I'm not sure whether I've built the architecture of the app correctly.

Let's say I have a user object stored in Redux. I created an async thunk function to fetch related information:

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

Correspondingly, I handled the pending/fulfilled/rejected callback as follows:

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
        })
    }
})

Considering modifying display status flags are quite common in other feature api implementations, I wrapped the two functions (setShowProgress() and clearError()) in reducers. Here comes my question: How can I reference the two functions in getUserInfo.pending function?

Though I could just assign the showProgress and error state variables in getUserInfo.pending instead of trying to call the reducer functions, that will definitely introduce duplicate code when I implement other fetching actions in the future. If it is not the recommended pattern, what is the best practice for this scenario?

like image 319
Sheffiled Avatar asked Jul 19 '20 07:07

Sheffiled


People also ask

What is createSlice in Redux toolkit?

A function that accepts an initial state, an object of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state. This API is the standard approach for writing Redux logic.

Can we write logic in reducer?

Most certainly! Reducers should be pure functions, so the logic must be pure as well.

What is extra reducers in Redux toolkit?

extraReducers ​ One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers can independently respond to the same action type. extraReducers allows createSlice to respond to other action types besides the types it has generated.


2 Answers

If your goal is just to set a loading boolean or error property based on pending/fulfilled/rejected, you can just use addMatcher which was introduced in version 1.4.

Here is a very basic example of using generic helpers to make this simple across many slices.

// 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 = '';
      });
    }
})
like image 98
Matt Sutkowski Avatar answered Oct 22 '22 07:10

Matt Sutkowski


I ran into a similar problem when replacing the old switch case approach with the new one using createReducer from redux-toolkit.

In the old approach, I often had a common reducer for many actions as shown below:

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;
  }
};

In the new approach, I would have to create two identical reducers and use them in .addCase. So I created a universal helper to match multiple actions:

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)
  )
);

And now we can easily use it with .addMatcher:

const authentication = createReducer(initialState, (builder) => (
  builder
    .addCase(signOutSuccess, () => ({
      ...initialState,
    }))
    .addMatcher(isOneOf([signInSuccess, signUpSuccess]), (state, action) => ({
      ...state,
      account: action.payload,
    }))
));
like image 36
Michał Wanat Avatar answered Oct 22 '22 07:10

Michał Wanat