reactjsreduxreact-reduxredux-thunkredux-toolkit

Common loading state reducer with Redux toolkit


I'm working on an app where I have multiple slices. I'm using createAsyncThunk for API calls and I like it cause it provides action creators for different state of API request, so that I can track loading state and errors within the reducer. But my question is, what if I want to have a separate reducer to track loading, error and success of my API calls how do I accomplish that with redux-toolkit

I know I can dispatch an action from within my createAsyncThunk function but it doesn't feel right and kinda defeats the purpose of the function itself. Also, side effects inside the reducer are considered to be a bad practice. So, I'm kinda confused at this point, I want to have just one Loader component in the root of the app that gets triggered when the loading state is true and it doesn't matter what exactly is loading

Here is an example of my current code:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { AxiosError } from 'axios'
import { masterInstance } from 'api'
import { GetAccessCodeParams, RegistrationStateType } from 'store/slices/registration/types'

export const getAccessCodeRequest = createAsyncThunk<void, GetAccessCodeParams, { rejectValue: { message: string } }>(
  'registration/getAccessCodeRequest',
  async ({ email }, { rejectWithValue }) => {
    try {
      await masterInstance.post(`/authorization/getAccessCodeWc`, { email })
    } catch (err) {
      let error: AxiosError = err
      if (error) {
        return rejectWithValue({
          message: `Error. Error code ${error.response?.status}`,
        })
      }
      throw err
    }
  }
)

const initialState: RegistrationStateType = {
  isLoading: false,
  error: null,
}

const registrationSlice = createSlice({
  name: 'registration',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(getAccessCodeRequest.fulfilled, (state) => {
      state.isLoading = false
      state.error = null
    })
    builder.addCase(getAccessCodeRequest.pending, (state) => {
      state.isLoading = true
      state.error = null
    })
    builder.addCase(getAccessCodeRequest.rejected, (state, action) => {
      if (action.payload) {
        state.error = {
          message: action.payload.message,
        }
      } else {
        state.error = action.error
      }
      state.isLoading = false
    })
  },
})

export const registrationReducer = registrationSlice.reducer

I want isLoading and error to be in a separate reducer


Solution

  • You could have a shared reducer matcher function.

    // mySharedStuff.js
    
    export const handleLoading = (action, (state) => {
      state.loading = action.type.endsWith('/pending'); // or smth similar
    });
    
    export const handleError = (action, (state) => {
      state.error = action.type.endsWith('/rejected'); // or smth similar
    });
    
    // mySlice.js
    
    const mySlice = createSlice({
      name: 'FOO',
      initialState: {},
      reducers: {},
      extraReducers: builder => {
        builder.addMatcher(handleLoading),
        builder.addMatcher(handleError),
        ...