Search code examples
reactjsreduxredux-thunkredux-toolkitredux-reducers

Pros & cons of using extra reducers vs dispatching within async thunk


I'm rather new to the whole React & Redux ecosystem & am trying to understand when & why to use extra reducers vs directly dispatching actions within an async thunk when working with the Redux toolkit.

Probably best explained with an example showing both solutions:

Version 1: Using extra reducers

auth.slice.ts

// ...

export const login = createAsyncThunk<LoginResponse, LoginData>(
  'auth/login',
  async ({ email, password }, thunkAPI) => {
    const data = await AuthService.login(email, password);

    // Extract user info from login response which holds other information as well
    // in which we're not interested in the auth slice...
    const userInfo = loginResponseToUserInfo(data);
    LocalStorageService.storeUserInfo(userInfo);

    // Return the whole login response as we're interested in the other data
    // besides the user info in other slices which handle `login.fulfilled` in
    // their own `extraReducers`
    return data;
  }
);

// ...

const authSlice = createSlice({
  // ...
  extraReducers: builder => {
    builder.addCase(login.fulfilled, (state, { payload }) => {
      // Again: Extract user info from login response which holds other
      // information as well in which we're not interested in the auth slice...
      const userInfo = loginResponseToUserInfo(payload);
      return { ...state, userInfo };
    }))
    // ...
  },
});

// ...

Version 2: Using dispatch inside async thunk

auth.slice.ts

// ...

export const login = createAsyncThunk<LoginResponse, LoginData>(
  'auth/login',
  async ({ email, password }, thunkAPI) => {
    const data = await AuthService.login(email, password);

    // Extract user info from login response which holds other information as well
    // in which we're not interested in the auth slice...
    const userInfo = loginResponseToUserInfo(data);
    LocalStorageService.storeUserInfo(userInfo);

    // !!! Difference to version 1 !!!
    // Directly dispatch the action instead of using `extraReducer` to further
    // process the extracted user info
    thunkAPI.dispatch(authSlice.actions.setUserInfo(userInfo));

    // Return the whole login response as we're interested in the other data
    // besides the user info in other slices which handle `login.fulfilled` in
    // their own `extraReducers`
    return data;
  }
);

// ...

const authSlice = createSlice({
  // ...
  reducers: {
    setUserInfo: (state, { payload }: PayloadAction<UserInfo>) => ({
      ...state,
      userInfo: payload,
    }),
    // ...
  },
});

// ...

Question

If I'm not completely wrong, both examples do the exact same thing but looking through the internet I mostly find people suggesting option 1 using the extraReducer which is why I'm asking:

  • Are both versions basically ok/correct or am I missing something?
  • Are there any benefits of sticking to the "extraReducers approach"?
    One minor drawback in this specific example is that I have to perform the conversion with loginResponseToUserInfo in 2 places (the async thunk & the extraReducer) whilst I only need to call it once in the 2nd version...

Solution

  • In my opinion both are valid although I would go for #1 personally.

    To justify my choice :

    • you should consider 'actions' as 'events', and the event is 'user logon successfull'. Having explicit action to set data into a slice is kinda wrong pattern
    • consider each of your slice as sub modules that should be able to work independently. The module in charge of authentication should not care if other slices are listening to its event or not ; in the future you might have other slices intereseted in this event and you dont want to end up with several extraneous dispatch in your thunk + the 'logon success' event might be triggered from another source.