Search code examples
javascriptreactjsreact-reduxredux-thunkredux-toolkit

Uncaught ReferenceError: Cannot access createAsyncThunk before initialization within a redux toolkit slice


I'm currently struggling to solve this error which i believe might be a circular reference dependency issue within my react redux application. It was working fine at first but then all of a sudden my app is throwing this error: enter image description here

From the error, it says checkAuth cannot be accessed before initialisation. checkAuth is a createAsyncThunk that is exported from authentication slice and is imported into the application slice to be added as extra reducers. Right now I am very confused on how to solve this issue. Hopefully you guys could help me out.

Here are the codes: store.js

import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./authentication/authenticationSlice";
import appReducer from "./application/appSlice";
import errorReducer from "./error/errorSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    error: errorReducer,
    app: appReducer,
  },
});

export default store;

authenticationSlice.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { axiosPublic, axiosPrivate } from "../../utils/axios";

const initialState = {
  isAuthLoading: false,
  authUser: null,
};

const checkAuth = createAsyncThunk("auth/checkAuth", async (_, thunkAPI) => {
  try {
    const response = await axiosPrivate.get("/auth");
    return response.data;
  } catch (e) {
    return thunkAPI.rejectWithValue(e.response?.data || e);
  }
});

const login = createAsyncThunk("auth/login", async (credentials, thunkAPI) => {
  try {
    const response = await axiosPublic.post("/auth/login", credentials, {
      withCredentials: true,
    });
    return response.data;
  } catch (e) {
    return thunkAPI.rejectWithValue(e.response?.data || e);
  }
});

const logout = createAsyncThunk("auth/logout", async (_, thunkAPI) => {
  try {
    await axiosPrivate.post("/auth/logout", null, { withCredentials: true });
    return;
  } catch (e) {
    return thunkAPI.rejectWithValue(e.response?.data || e);
  }
});

const reqOTPHandler = createAsyncThunk(
  "auth/reqOTPHandler",
  async ({ endpoint, credentials }, thunkAPI) => {
    try {
      await axiosPublic.post(endpoint, credentials);
      return;
    } catch (e) {
      return thunkAPI.rejectWithValue(e.response?.data || e);
    }
  }
);

const signup = createAsyncThunk(
  "auth/submitOTP",
  async (credentials, thunkAPI) => {
    try {
      const response = await axiosPublic.post(
        "/auth/confirm-signup",
        credentials,
        {
          withCredentials: true,
        }
      );
      return response.data;
    } catch (e) {
      return thunkAPI.rejectWithValue(e.response?.data || e);
    }
  }
);

const reqResetHandler = createAsyncThunk(
  "/auth/reqResetHandler",
  async ({ endpoint, token }, thunkAPI) => {
    try {
      await axiosPublic.post(endpoint, {
        token,
      });
      return;
    } catch (e) {
      return thunkAPI.rejectWithValue(e.response?.data || e);
    }
  }
);

const resetPassword = createAsyncThunk(
  "auth/resetPassword",
  async ({ endpoint, credentials }, thunkAPI) => {
    try {
      await axiosPublic.post(endpoint, credentials);
      return;
    } catch (e) {
      return thunkAPI.rejectWithValue(e.response?.data || e);
    }
  }
);

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    loading: (state) => {
      state.isAuthLoading = true;
    },
    idle: (state) => {
      state.isAuthLoading = false;
    },
    updateAuthUser: (state, action) => {
      state.authUser = action.payload;
    },
    removeAuthUser: (state) => {
      state.authUser = null;
    },
  },
  extraReducers: {
    [checkAuth.fulfilled]: (state, action) => {
      state.authUser = action.payload;
    },

    [login.pending]: (state) => {
      state.isAuthLoading = true;
    },
    [login.fulfilled]: (state, action) => {
      state.isAuthLoading = false;
      state.authUser = action.payload;
    },
    [login.rejected]: (state, action) => {
      state.isAuthLoading = false;
    },

    [logout.fulfilled]: (state, action) => {
      state.authUser = null;
    },

    [reqOTPHandler.pending]: (state) => {
      state.isAuthLoading = true;
    },
    [reqOTPHandler.fulfilled]: (state) => {
      state.isAuthLoading = false;
    },
    [reqOTPHandler.rejected]: (state) => {
      state.isAuthLoading = false;
    },

    [signup.pending]: (state) => {
      state.isAuthLoading = true;
    },
    [signup.fulfilled]: (state, action) => {
      state.isAuthLoading = false;
      state.authUser = action.payload;
    },
    [signup.rejected]: (state) => {
      state.isAuthLoading = false;
    },

    [reqResetHandler.pending]: (state) => {
      state.isAuthLoading = true;
    },
    [reqResetHandler.fulfilled]: (state, action) => {
      state.isAuthLoading = false;
    },
    [reqResetHandler.rejected]: (state) => {
      state.isAuthLoading = false;
    },

    [resetPassword.pending]: (state) => {
      state.isAuthLoading = true;
    },
    [resetPassword.fulfilled]: (state, action) => {
      state.isAuthLoading = false;
    },
    [resetPassword.rejected]: (state) => {
      state.isAuthLoading = false;
    },
  },
});

const { reducer } = authSlice;

const { loading, idle, updateAuthUser, removeAuthUser } = authSlice.actions;

export {
  loading as authLoading,
  idle as authIdle,
  updateAuthUser,
  removeAuthUser,
  // thunks
  checkAuth,
  login,
  logout,
  reqOTPHandler,
  signup,
  reqResetHandler,
  resetPassword,
};

export default reducer;

appSlice.js

import { createSlice } from "@reduxjs/toolkit";
import { checkAuth, logout } from "../authentication/authenticationSlice";

const initialState = {
  // Global App loading state
  isInitializing: true,
  isAppLoading: false,
  currentBranch: null,
  currentRole: null,
};

const appSlice = createSlice({
  name: "app",
  initialState,
  reducers: {
    appInitializing: (state) => {
      state.isInitializing = true;
    },
    appInitialized: (state) => {
      state.isInitializing = false;
    },
    appLoading: (state) => {
      state.isAppLoading = true;
    },
    appFullfilled: (state) => {
      state.isAppLoading = false;
    },
  },
  extraReducers: {
    // auth
    [checkAuth.pending]: (state) => {
      state.isInitializing = true;
    },
    [checkAuth.fulfilled]: (state) => {
      state.isInitializing = false;
    },
    [checkAuth.rejected]: (state) => {
      state.isInitializing = false;
    },

    [logout.pending]: (state) => {
      state.isAppLoading = true;
    },
    [logout.fulfilled]: (state) => {
      state.isAppLoading = false;
    },
    [logout.rejected]: (state) => {
      state.isAppLoading = false;
    },
  },
});

const { reducer } = appSlice;

export const { appInitializing, appInitialized, appLoading, appFullfilled } =
  appSlice.actions;

export default reducer;

Solution

  • Don't use the extraReducers object notation - that one will be deprecated soon as it doesn't work with TypeScript - and has this exact problem. If you use the builder notation instead, that will work.

    extraReducers: builder => {
        // auth
        builder.addCase(checkAuth.pending. (state) => {
          state.isInitializing = true;
        });
        ...
    }