Search code examples
javascriptreactjsreduxredux-toolkitredux-thunk

A action from one slice is changing the state of another slice using redux toolkit


I'm using redux toolkit.

I have different slices and they have their own states.

I have a slice roles

import { createSlice } from '@reduxjs/toolkit';
import { getAllRoles } from './roleActions';

const initialState = {
  roles: [],
  loading: false,
  isSuccess: false,
  message: '',
};

const authReducer = createSlice({
  name: 'roles',
  initialState,

  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getAllRoles.pending, (state) => {
        state.isSuccess = false;
        state.message = '';
        state.loading = true;
      })
      .addCase(getAllRoles.fulfilled, (state, { payload }) => {
        state.roles = payload.data;
        state.message = payload.message;
        state.isSuccess = true;
        state.loading = false;
      })
      .addCase(getAllRoles.rejected, (state, { payload }) => {
        state.isSuccess = false;
        state.message = payload;
        state.loading = false;
      });
  },
});

export default authReducer.reducer;

Here is the the getAllRoles action

import { createAsyncThunk } from '@reduxjs/toolkit';
import API from '../../api';
import { formatError } from '@utils/index';

export const getAllRoles = createAsyncThunk('role/getAllRoles', async (_, { rejectWithValue }) => {
  try {
    const { data } = await API.get('/roles');
    return data;
  } catch (error) {
    return rejectWithValue(formatError(error));
  }
});

I'm fetching all roles as:

useEffect(() => {
    dispatch(getAllRoles());
  }, [dispatch]);

Another slice:

import { createSlice } from '@reduxjs/toolkit';
import { deleteJob, getJobs, updateJob, createJob } from './jobActions';

const initialValues = {
  data: {
    jobs: [],
    totalCount: 0,
  },
  loading: false,
  isSuccess: false,
  message: '',
};

const jobsReducer = createSlice({
  name: 'jobs',
  initialState: initialValues,
  reducers: {},
  extraReducers: (builder) => {
    // Get Jobs
    builder
      .addCase(getJobs.fulfilled, (state, { payload }) => {
        return {
          ...state,
          data: payload.data,
          message: payload.message,
          isSuccess: true,
          loading: false,
        };
      })
      .addCase(deleteJob.fulfilled, (state, { payload }) => {
        const updatedJobs = [...state.data.jobs.filter((job) => job._id !== payload)];
        return {
          ...state,
          data: {
            totalCount: state.data.totalCount - 1,
            jobs: updatedJobs,
          },
          message: 'Job deleted successfully',
          isSuccess: true,
          loading: false,
        };
      })
      .addCase(updateJob.fulfilled, (state, { payload }) => {
        const updatedJobs = [...state.data.jobs.filter((job) => job._id !== payload.data._id), payload.data];
        return {
          ...state,
          data: {
            totalCount: state.data.totalCount,
            jobs: updatedJobs,
          },
          message: payload.message,
          isSuccess: true,
          loading: false,
        };
      })
      .addCase(createJob.fulfilled, (state, { payload }) => {
        return {
          ...state,
          data: {
            totalCount: state.data.totalCount + 1,
            jobs: [payload.data, ...state.data.jobs],
          },
          message: payload.message,
          isSuccess: true,
          loading: false,
        };
      })
      .addMatcher(
        (action) => action.type.endsWith('/pending'),
        (state) => ({
          ...state,
          isSuccess: false,
          message: '',
          loading: true,
        }),
      )
      .addMatcher(
        (action) => action.type.endsWith('/rejected'),
        (state, { payload }) => ({
          ...state,
          isSuccess: false,
          message: payload,
          loading: false,
        }),
      );
  },
});

export default jobsReducer.reducer;

One more another slice:

import { createUser, deleteUser, getUsers, updateUser } from './userActions';

const initialState = {
  users: [],
  loading: false,
  isSuccess: false,
  message: '',
};

const usersReducer = createSlice({
  name: 'users',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    // Get Users
    builder.addCase(getUsers.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users = payload.data;
      state.isSuccess = true;
    });

    // Delete Users
    builder.addCase(deleteUser.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users = [...state.users.filter((user) => user._id !== payload)];
      state.isSuccess = true;
    });

    // Update User
    builder.addCase(updateUser.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users.splice(
        state.users.findIndex((user) => user.email === payload.data.email),
        1,
        payload.data,
      );
      state.isSuccess = true;
      state.message = payload.message;
    });

    // Create User
    builder.addCase(createUser.fulfilled, (state, { payload }) => {
      state.loading = false;
      state.message = payload.message;
      state.users.unshift(payload.data);
      state.isSuccess = true;
    });

    // Pending State
    builder.addMatcher(
      (action) => action.type.endsWith('/pending'),
      (state) => {
        state.isSuccess = false;
        state.message = '';
        state.loading = true;
      },
    );

    // Rejected State
    builder.addMatcher(
      (action) => action.type.endsWith('/rejected'),
      (state, { payload }) => {
        state.isSuccess = false;
        state.message = payload;
        state.loading = false;
      },
    );
  },
});

export default usersReducer.reducer;

Now the issue is when getAllRoles triggeres it should change its loading state to true that it does correctly,

but it also chnages the loading state of users and jobs to true, only the loading state and it doesn't not set back to false.

and same like i have some other slices they are also behaving like this,

and when I dispatch the action of getJobs

now it has also different behaviour like it chnages the loading state of users to true but not of roles or any other.

Here is redux devtools screenshot:

enter image description here

I have selected roles/getAllRoles/pending action and check the state difference it is changing the loading state of othere slices also which it shouln't.

Here is my store configuration:

import storage from 'redux-persist/lib/storage';
import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';
import thunk from 'redux-thunk';
import { authReducer, usersReducer, rolesReducer, jobsReducer, surveysReducer, pageInfoReducer } from './features';

const reducers = combineReducers({
  auth: authReducer,
  users: usersReducer,
  roles: rolesReducer,
  jobs: jobsReducer,
  survey: surveysReducer,
  pageInfo: pageInfoReducer,
});

const store = configureStore({
  reducer: reducers,
  devTools: process.env.NODE_ENV !== 'production',
  middleware: [thunk],
});

export default store;

Tried removing addMatcher, using unique names for actions and slices, tried using builder.addCase seprately instead of in chaning way. Nothing works.


Solution

  • Issue

    Both the jobs and users state slices have reducers cases defined that handle any pending or rejected action.

    const jobsReducer = createSlice({
      name: 'jobs',
      initialState: initialValues,
      reducers: {},
      extraReducers: (builder) => {
        // Get Jobs
        builder
          ...
          .addMatcher(
            (action) => action.type.endsWith('/pending'), // <-- any pending
            (state) => ({
              ...state,
              isSuccess: false,
              message: '',
              loading: true,
            }),
          )
          .addMatcher(
            (action) => action.type.endsWith('/rejected'), // <-- any rejected
            (state, { payload }) => ({
              ...state,
              isSuccess: false,
              message: payload,
              loading: false,
            }),
          );
      },
    });
    
    const usersReducer = createSlice({
      name: 'users',
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        ...
    
        // Pending State
        builder.addMatcher(
          (action) => action.type.endsWith('/pending'), // <-- any pending
          (state) => {
            state.isSuccess = false;
            state.message = '';
            state.loading = true;
          },
        );
    
        // Rejected State
        builder.addMatcher(
          (action) => action.type.endsWith('/rejected'), // <-- any rejected
          (state, { payload }) => {
            state.isSuccess = false;
            state.message = payload;
            state.loading = false;
          },
        );
      },
    });
    

    When 'role/getAllRoles/pending' is dispatched to the store, these other reducer cases will, with the current code, correctly update and set their local loading state (as well as the state updates in their cases). If the asynchronous getAllRoles action completes successfully, there isn't a case for this in these other state slices to clear their loading states.

    Solution

    Recall that all reducers in the reducer tree of the Redux store are passed each action that is dispatched to the store, and only reducers with cases for specific actions respond.

    I suspect you are interested in only the pending/rejected status of the deleteJob, getJobs, updateJob, and createJob actions in the case of the jobs slice, and only the pending/rejected status of the createUser, deleteUser, getUsers, and updateUser in the case of the users slice. For this I suggest using the isPending, isFulfilled, and isRejected matching utilities.

    Example:

    import { createSlice, isPending, isFulfilled, isRejected } from '@reduxjs/toolkit';
    import { deleteJob, getJobs, updateJob, createJob } from './jobActions';
    
    const initialState = {
      data: {
        jobs: [],
        totalCount: 0,
      },
      loading: false,
      isSuccess: false,
      message: '',
    };
    
    const jobsReducer = createSlice({
      name: 'jobs',
      initialState,
      extraReducers: (builder) => {
        // Get Jobs
        builder
          .addCase(getJobs.fulfilled, (state, { payload }) => {
            return {
              ...state,
              data: payload.data,
              message: payload.message,
            };
          })
          .addCase(deleteJob.fulfilled, (state, { payload }) => {
            const updatedJobs = state.data.jobs.filter((job) => job._id !== payload);
            return {
              ...state,
              data: {
                totalCount: state.data.totalCount - 1,
                jobs: updatedJobs,
              },
              message: 'Job deleted successfully',
            };
          })
          .addCase(updateJob.fulfilled, (state, { payload }) => {
            return {
              ...state,
              data: {
                totalCount: state.data.totalCount,
                jobs: state.data.jobs
                  .filter((job) => job._id !== payload.data._id)
                  .concat(payload.data),
              },
              message: payload.message,
            };
          })
          .addCase(createJob.fulfilled, (state, { payload }) => {
            return {
              ...state,
              data: {
                totalCount: state.data.totalCount + 1,
                jobs: [payload.data, ...state.data.jobs],
              },
              message: payload.message,
            };
          })
          .addMatcher(
            isPending(deleteJob, getJobs, updateJob, createJob),
            (state) => {
              state.isSuccess: false,
              state.message: '',
              state.loading: true,
            },
          )
          .addMatcher(
            isFulfilled(deleteJob, getJobs, updateJob, createJob),
            (state) => {
              state.isSuccess: true,
              state.loading: false,
            },
          )
          .addMatcher(
            isRejected(deleteJob, getJobs, updateJob, createJob),
            (state, { payload }) => {
              state.isSuccess: false,
              state.message: payload,
              state.loading: false,
            },
          );
      },
    });
    
    import { createSlice, isPending, isFulfilled, isRejected } from '@reduxjs/toolkit';
    import { createUser, deleteUser, getUsers, updateUser } from './userActions';
    
    const initialState = {
      users: [],
      loading: false,
      isSuccess: false,
      message: '',
    };
    
    const usersReducer = createSlice({
      name: 'users',
      initialState,
      extraReducers: (builder) => {
        // Get Users
        builder.addCase(getUsers.fulfilled, (state, { payload }) => {
          state.message = payload.message;
          state.users = payload.data;
        });
    
        // Delete Users
        builder.addCase(deleteUser.fulfilled, (state, { payload }) => {
          state.message = payload.message;
          state.users = [...state.users.filter((user) => user._id !== payload)];
        });
    
        // Update User
        builder.addCase(updateUser.fulfilled, (state, { payload }) => {
          state.message = payload.message;
          state.users.splice(
            state.users.findIndex((user) => user.email === payload.data.email),
            1,
            payload.data,
          );
        });
    
        // Create User
        builder.addCase(createUser.fulfilled, (state, { payload }) => {
          state.message = payload.message;
          state.users.unshift(payload.data);
        });
    
        // Pending State
        builder.addMatcher(
          isPending(createUser, deleteUser, getUsers, updateUser),
          (state) => {
            state.isSuccess = false;
            state.message = '';
            state.loading = true;
          },
        );
    
        // Fulfilled State
        builder.addMatcher(
          isFulfilled(createUser, deleteUser, getUsers, updateUser),
          (state) => {
            state.isSuccess = true;
            state.message = '';
            state.loading = false;
          },
        );
    
        // Rejected State
        builder.addMatcher(
          isRejected(createUser, deleteUser, getUsers, updateUser),
          (state, { payload }) => {
            state.isSuccess = false;
            state.message = payload;
            state.loading = false;
          },
        );
      },
    });