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
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>