Search code examples
javascriptreactjsreact-router-dom

React Router Dom Protected Routes Keep redirecting


I am making protected routes. am using redux and pure react app and React Router Dom. The problem am face when I go to /profile/edit and hit refresh then it redirects me to /login > /profile. I created a protected route. I want to make something like when I am on isAuthenticated page it will stay on that page. I don't want it to redirect me to /profile. I put my protectedRoute in element and having the issue. Now I am using it inside of loader. but now my protected route is not working. Can someone please tell me how I can do this? or give me any idea? Thank you

// App.js

<Routes>
  <Route
    element={
      <ProtectedRoute
        isAuthenticatedRoute={false}
        sendTo={"/profile"}
      />
    }
  >
    <Route path="/login" element={<Login />} />
    <Route path="/register" element={<Register />} />
  </Route>

  {/* Profile Pages */}
  <Route
    element={
      <ProtectedRoute
        isAuthenticatedRoute={true}
        sendTo={"/login"}
      />
    }
  >
    <Route path="/profile" element={<Profile />} />
    <Route path="/profile/category" element={<EditCategory />} />
    <Route path="/profile/edit" element={<EditProfile />} />
  </Route>

  {/* Page Not Found */}
  <Route path="/*" element={<PageNotFound />} />
</Routes>

// App.js with loader

<Routes>
  <Route
    loader={
      <ProtectedRoute
        isAuthenticatedRoute={false}
        sendTo={"/profile"}
      />
    }
  >
    <Route path="/login" element={<Login />} />
    <Route path="/register" element={<Register />} />
  </Route>
</Routes>

// ProtectedRoutes.js

import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import { Navigate, Outlet, useNavigate } from "react-router-dom";
import AppLoader from "../AllLoader/AppLoader/AppLoader";
import { useLoadUserQuery } from "../../redux/feature/apiSlice/apiSlice";

const ProtectedRoute = ({
  isAuthenticatedRoute,
  children,
  adminRoute,
  isAdmin,
  sendTo,
}) => {
  const navigate = useNavigate();
  const { isLoading } = useLoadUserQuery(undefined, undefined);

  const { user } = useSelector((state) => state.auth);

  if (isLoading) {
    return <AppLoader />;
  }

  const isAuthenticated = user;

  if (isAuthenticatedRoute && !isAuthenticated) {
    return <Navigate to="/login" replace={true} />;
  }
  if (!isAuthenticatedRoute && isAuthenticated) {
    return <Navigate to="/profile" replace={true} />;
  }

  return children ? children : <Outlet />;
};

export default ProtectedRoute;
My redux store
import { configureStore } from "@reduxjs/toolkit";
import newsSliceReducer from "./feature/newsSlice/newsSlice";
import authSliceReducer from "./feature/userSlice/authSlice";
import profileSliceReducer from "./feature/profileSlice/profileSlice";
import languageSliceReducer from "./feature/languageSlice/languageSlice";
import otherSliceReducer from "./feature/otherSlice/otherSlice";
import { apiSlice } from "./feature/apiSlice/apiSlice";
import { getUserProfile, refreshToken } from "./feature/userSlice/authApi";

const store = configureStore({
    reducer: {
        [apiSlice.reducerPath]: apiSlice.reducer,
        // For Getting all news
        allNews: newsSliceReducer,

        // For Authorization
        auth: authSliceReducer,

        // For user Profile
        profile: profileSliceReducer,

        // For others Data
        other: otherSliceReducer,

        // For Langauge which will be used later
        languages: languageSliceReducer,
    },

    // devTools: false,
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(apiSlice.middleware),
});

// Here will call the Get profile and refresh topke

const initializeApp = async () => {
    await store.dispatch(
        apiSlice.endpoints.refreshToken.initiate({}, { forceRefetch: true })
    );
    await store.dispatch(
        apiSlice.endpoints.loadUser.initiate({}, { forceRefetch: true })
    );

    // await store.dispatch(refreshToken());
    // await store.dispatch(getUserProfile());
};

initializeApp();

export default store;


//apiSlice

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { SERVER } from "../../../utils/backend";
import { userValue } from "../userSlice/authSlice";

export const apiSlice = createApi({
    reducerPath: "api",
    baseQuery: fetchBaseQuery({ baseUrl: SERVER }),
    endpoints: (builder) => ({
        refreshToken: builder.query({
            query: (data) => ({
                url: "/user/refresh-token",
                method: "GET",
                credentials: "include",
            }),
        }),

        // Load users
        loadUser: builder.query({
            query: (data) => ({
                url: "/user/me",
                method: "GET",
                credentials: "include",
            }),

            async onQueryStarted(arg, { queryFulfilled, dispatch }) {
                try {
                    const result = await queryFulfilled;
                    dispatch(
                        userValue({
                            token: result.data.accessToken,
                            user: result.data.user,
                        })
                    );
                } catch (error) {
                    console.log(error.message);
                }
            },
        }),
    }),
});

export const { useRefreshTokenQuery, useLoadUserQuery } = apiSlice;


My ApiSlice
import { createSlice } from "@reduxjs/toolkit";
import {
    activateUser,
    getUserProfile,
    refreshToken,
    userLogin,
    userLogout,
    userRegister,
} from "./authApi";

const userSlice = createSlice({
    name: "user",
    initialState: {
        authLoading: false,
        getUserProfileLoading: false,
        refreshTokenLoading: false,
        user: null,
        userActivationToken: "",
        authError: null,
        authMessage: null,
        getUserProfileMessage: null,
        getUserProfileError: null,
    },
    reducers: {
        clearAuthMessage: (state) => {
            state.authMessage = null;
        },
        clearAuthError: (state) => {
            state.authError = null;
        },
        clearUserProfileError: (state) => {
            state.getUserProfileError = null;
        },

        userValue: (state, action) => {
            state.user = action.payload.user;
        },
    },
    extraReducers: (builder) => {
        // Register

        builder.addCase(userRegister.pending, (state) => {
            state.authLoading = true;
        });
        builder.addCase(userRegister.fulfilled, (state, action) => {
            state.authLoading = false;
            state.authMessage = action.payload.message;
            state.userActivationToken = action.payload.activationToken;
        });

        builder.addCase(userRegister.rejected, (state, action) => {
            state.authLoading = false;
            state.authError = action.payload.message;
        });

        // Login

        builder.addCase(userLogin.pending, (state) => {
            state.authLoading = true;
        });
        builder.addCase(userLogin.fulfilled, (state, action) => {
            state.authLoading = false;
            state.user = action.payload.user;
            state.authMessage = action.payload.message;
        });

        builder.addCase(userLogin.rejected, (state, action) => {
            state.authLoading = false;
            state.authError = action.payload.message;
        });

        // Logout =================================================================

        builder.addCase(userLogout.pending, (state) => {
            state.authLoading = true;
        });
        builder.addCase(userLogout.fulfilled, (state, action) => {
            state.authLoading = false;
            state.user = null;
            state.authMessage = action.payload.message;
        });

        builder.addCase(userLogout.rejected, (state, action) => {
            state.authLoading = false;
            state.authError = action.payload.message;
        });

        // =========================    Active User        ========================================

        builder.addCase(activateUser.pending, (state) => {
            state.authLoading = true;
        });

        builder.addCase(activateUser.fulfilled, (state, action) => {
            state.authLoading = false;
            state.authMessage = action.payload.message;
        });

        builder.addCase(activateUser.rejected, (state, action) => {
            state.authLoading = false;
            state.authError = action.payload.message;
        });

        // =========================    Refresh token     ========================================

        builder.addCase(refreshToken.pending, (state) => {
            state.refreshTokenLoading = true;
        });

        builder.addCase(refreshToken.fulfilled, (state) => {
            state.refreshTokenLoading = false;
        });

        builder.addCase(refreshToken.rejected, (state) => {
            state.refreshTokenLoading = false;
        });

        //========================================  Get User Profile     ========================================

        builder.addCase(getUserProfile.pending, (state) => {
            state.getUserProfileLoading = true;
        });
        builder.addCase(getUserProfile.fulfilled, (state, action) => {
            state.getUserProfileLoading = false;
            state.user = action.payload.user;
        });

        builder.addCase(getUserProfile.rejected, (state, action) => {
            state.getUserProfileLoading = false;
            state.getUserProfileError = action.payload.message;
        });
    },
});

export const {
    clearAuthMessage,
    clearAuthError,
    clearUserProfileError,
    userValue,
} = userSlice.actions;

export default userSlice.reducer;


My index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import { Provider } from "react-redux";
import store from "./redux/store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <React.StrictMode>
        <Provider store={store}>
            <BrowserRouter>
                <App />
            </BrowserRouter>
        </Provider>
    </React.StrictMode>
);

reportWebVitals();

Am expecting when am on any authenticated page, it will stay there. It won't redirect me at first /login > /profile page


Solution

  • I suspect the useLoadUserQuery query's isLoading status isn't set true until after the initial render cycle has completed and the query could be initiated in the component. I suspect that on the initial render cycle that isLoading is false and that ProtectedRoute uses the initial state value null of the selected state.auth.user value that results in the rendering of one of the Navigate components to redirect the user.

    My suggestion would be to start state.auth.user from an indeterminant value, e.g. undefined and also explicitly check for this value when loading.

    Example:

    const userSlice = createSlice({
      name: "user",
      initialState: {
        authLoading: false,
        getUserProfileLoading: false,
        refreshTokenLoading: false,
        user: undefined, // <-- initially undefined
        userActivationToken: "",
        authError: null,
        authMessage: null,
        getUserProfileMessage: null,
        getUserProfileError: null,
      },
      reducers: {
        ...
      },
      extraReducers: (builder) => {
        ...
      },
    });
    
    const ProtectedRoute = ({
      isAuthenticatedRoute,
      children,
      adminRoute,
      isAdmin,
      sendTo,
    }) => {
      const navigate = useNavigate();
      const { isLoading } = useLoadUserQuery(undefined, undefined);
    
      const user = useSelector((state) => state.auth.user);
    
      if (isLoading || user === undefined) { // <-- loading, or undefined user
        return <AppLoader />;
      }
    
      const isAuthenticated = !!user;
    
      if (isAuthenticatedRoute && !isAuthenticated) {
        return <Navigate to="/login" replace />;
      }
      if (!isAuthenticatedRoute && isAuthenticated) {
        return <Navigate to="/profile" replace />;
      }
    
      return children ?? <Outlet />;
    };
    

    A more conventional route protection implementation would separate the logic of protecting certain routes from unauthenticated users from the logic of protecting certain routes from authenticated users. Modify ProtectedRoute to only redirect unauthenticated users and create another component to do the inverse for the login-type routes.

    Example:

    const ProtectedRoute = ({ children }) => {
      const navigate = useNavigate();
      const { isLoading } = useLoadUserQuery(undefined, undefined);
    
      const user = useSelector((state) => state.auth.user);
    
      if (isLoading || user === undefined) {
        return <AppLoader />;
      }
    
      const isAuthenticated = !!user;
      
      if (!isAuthenticated) {
        return <Navigate to="/login" replace />;
      }
    
      return children ?? <Outlet />;
    };
    
    const AnonymousRoute = ({ children }) => {
      const navigate = useNavigate();
      const { isLoading } = useLoadUserQuery(undefined, undefined);
    
      const user = useSelector((state) => state.auth.user);
    
      if (isLoading || user === undefined) {
        return <AppLoader />;
      }
    
      const isAuthenticated = !!user;
    
      if (isAuthenticated) {
        return <Navigate to="/profile" replace />;
      }
    
      return children ?? <Outlet />;
    };
    
    <Routes>
      <Route element={<AnonymousRoute />}>
        <Route path="/login" element={<Login />} />
        <Route path="/register" element={<Register />} />
      </Route>
    
      <Route element={<ProtectedRoute/>}>
        <Route path="/profile" element={<Profile />} />
        <Route path="/profile/category" element={<EditCategory />} />
        <Route path="/profile/edit" element={<EditProfile />} />
      </Route>
    
      <Route path="/*" element={<PageNotFound />} />
    </Routes>