I want to render outlets based on user login state. The problem is the oulet never get rendered. Only parent element is rendered.I tried using "/:username/boards/*" for path. But that didn't work either.
App.js
<Route element={<ProtectedOutlet />}>
<Route path="/:username/boards" element={<UserHome logOut={actions.logOut} getAllBoards={actions.getAllBoards} />} >
<Route path="b" element={<Board />} />
</Route>
</Route>
ProtectedOutlet.js
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const ProtectedOutlet = () => {
const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
}
export default ProtectedOutlet;
UserHome.js
const UserHome = (props) => {
return (
<div className="user-home">
<Navbar />
<Outlet />
</div>
)
}
Problem solved. I changed my codes in ProtectedOutlet
from
const ProtectedOutlet = () => {
const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
}
to
const ProtectedOutlet = () => {
const { isLoggedIn, user } = useSelector(state => state.auth);
if (!isLoggedIn && !user) return <h1>Loading</h1>
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
}
And I've other route for unauthenticated routes like (/login,/register, etc..) called PublicOutlet.js.
PublicOutlet.js
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const PublicOutlet = () => {
const { isLoggedIn, user } = useSelector(state => state.auth);
return isLoggedIn ? <Navigate to={`/${user.username}/boards`}
replace={true} /> : <Outlet />;
}
export default PublicOutlet;
The problem is when I enter url in url bar, the protectedoutlet component check if the user is logged in or not. When it is rendered the first time, isLoggedIn state is not updated yet. so it get navigated to /login route. Then /login route is wrapped in publicoutlet component. When publicoutlet component check if the user is is logged in, isLoggedIn state changed to true. So, it get redirected to /${user.username}/boards. It took me 2days to find out the problem. LOL
Related
After successful login how can I see/access Dashboard, CreatedLink components mentioned inside the protected route. Could someone please advise how do I check
loginEmail is available in localStorage then display the above components else display login. Do I need another component for navigation to achieve this ?
App.js
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Dashboard from "./components/dashboard";
import CreateLink from "./components/createLink";
import Nominate from "./components/nominate";
import Login from "./components/login";
import { ProtectedRoute } from "./components/protectedRoute";
import ErrorPage from "./components/errorPage";
function App() {
return (
<Router>
<div>
<div className="navbar-nav">
</div>
<Switch>
<ProtectedRoute exact path='/dashboard' component={Dashboard} />
<ProtectedRoute exact path='/createLink' component={CreateLink} />
<Route exact path='/' component={Login} />
<Route path='/nominate/:token' component={Nominate} />
<Route path='/errorPage' component={ErrorPage} />
</Switch>
</div>
</Router>
);
}
export default App;
login.js
import React, { useRef, useEffect, useState } from "react";
import { useGoogleLogin } from 'react-google-login';
import { refreshToken } from '../utils/refreshToken';
const clientId ="client_id_here";
const Login = () => {
const [userEmail, setUserEmail] = useState("");
const onSuccess = (res) =>{
console.log("Login successfully",res.profileObj);
const email = res.profileObj.email;
setUserEmail(email);
window.localStorage.setItem("loginEmail", email);
refreshToken(res);
}
const onFailure = (res) => {
console.log('Login failed: res:', res);
alert(
`Failed to login !`
);
};
const {signIn} = useGoogleLogin ({
onSuccess,
onFailure,
clientId,
isSignedIn: true,
accessType: 'offline',
})
return (
<div className="App">
<h1>Login</h1>
<div className="inputForm">
<button onClick={signIn}>
<img src="images/google.png" className="loginG"/>
<span className="loginText">Sign in</span>
</button>
</div>
</div>
)
}
export default Login;
protectedRoute.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
if (localStorage.getItem("loginEmail")) {
return <Component {...props} />;
} else {
return (
<>
<Redirect
to={{
pathname: "/",
state: {
from: props.location,
},
}}
/>
</>
);
}
}}
/>
);
};
Sandboxlink
https://codesandbox.io/s/gracious-forest-4csz3?file=/src/App.js
If I understand your question correctly, you want to be routed back to the protected route originally being accessed before getting bounced to the login route.
The protected route passed a referrer, i.e. from value in route state, you just need to access this value and imperatively redirect back to the original route.
login
Access the location.state and history objects to get the referrer value and redirect to it. This destructures from from location.state object and provides a fallback to the "/dashboard" route if the referrer is undefined (i.e. user navigated directly to "/login").
import { useHistory, useLocation } from 'react-router-dom';
const Login = () => {
const history = useHistory();
const { state: { from = "/dashboard" } = {} } = useLocation();
...
const onSuccess = (res) => {
console.log("Login successfully", res.profileObj);
const email = res.profileObj.email;
setUserEmail(email);
window.localStorage.setItem("loginEmail", email);
// refreshToken(res);
history.replace(from);
};
I also think a small refactor of your PrivateRoute component will allow you to first check localStorage when landing on a protected route.
export const ProtectedRoute = (props) => {
return localStorage.getItem("loginEmail") ? (
<Route {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
};
I'm creating a protected route for my react project but context values and redux reducers data are not persistent. So what is the optimal way to set, for example isVerified to true if the user is logged. If isVerified === true go to homepage else redirect to login, isVerified needs to be mutated every change in route or refresh, because context or redux data is not persistent.
Do I need to create a separate backend api for just checking the token coming from the client side? then I will add a useEffect in the main App.tsx, something like:
useEffect(() => {
/*make api call, and pass the token stored in the localStorage. If
verified success then: */
setIsVerified(true)
}, [])
Then I can use the isVerified to my protected route
You can create a Middleware component wrapping both, Protected and Non-protected components routes. And inside of each just check if the user is logged, then render conditionally.
This is usually how I implemented,
Protected:
// AuthRoute.js
import React, { useEffect, useState } from "react";
import { Redirect, Route } from "react-router-dom";
export const AuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ? (
<Route exact={exact} path={path} component={component} />
) : (
<Redirect to={"/login"} />
);
};
Non-protected:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { useEffect, useState } from "react";
export const NAuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ?
<Redirect to={"/"} />
: <Route exact={exact} path={path} component={component} />;
};
I usually use this process and so far it is the most helpful way to make a route protected.
ProtectedRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export const ProtectedRoute = ({component: Component, ...rest}) => {
const isSessionAuth = sessionStorage.getItem('token');
return (
<Route
{...rest}
render={props => {
if(isSessionAuth)
{
return (<Component {...props}/>);
}else{
return <Redirect to={
{
pathname: "/",
state: {
from: props.location
}
}
}/>
}
}
}/>
);
};
In the place where you are defining routes you can use this like this.
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute.js';
import SignUp from "./pages/SignUp";
import SignIn from "./pages/SignIn";
import Dashboard from "./pages/Dashboard";
import NotFound from "./pages/NotFound";
const Routes = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={SignIn} />
<ProtectedRoute exact path="/dashboard" component={Dashboard} />
<Route path="/signup" component={SignUp} />
<Route path="*" component={NotFound} />
</Switch>
</BrowserRouter>
);
export default Routes;
I'm using reach router for my routes. I was able to protect the dashboard and expose the login page and redirect if the user is not logged in but now if I enter a url it will do a quick redirecto to login and then to home instead of the page actually entered.
I noticed because of the useEffect to fetch the user the component renders twice: 1 without user (redirects to login) the other one with user and redirects to home.
Routes file
const AdminRoutes = () => {
return (
<Router>
<MainLayout path="/admin">
<HomePage path="/" />
<CarTransfer path="/cartransfers">
<CarTranserList path="/list" />
<CarTransferCreate path="/new" />
</CarTransfer>
<User path="/users">
<UserList path="/list" />
<UserCreate path="/new" />
</User>
</MainLayout>
<LoginPage path="/login" />
</Router>
);
};
Layout file
import { useState, useEffect } from "react";
import { Redirect, useNavigate } from "#reach/router";
import { Layout } from "antd";
import SiderMenu from "./SiderMenu";
import LayoutBanner from "./LayoutBanner";
import { useSelector, useDispatch } from "react-redux";
import {
userSelector,
fetchUserBytoken,
clearState,
} from "../../features/authSlice";
const { Content } = Layout;
const MainLayout = ({ children }) => {
const user = useSelector(userSelector);
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const { isFetching, isError } = useSelector(userSelector);
useEffect(() => {
dispatch(
fetchUserBytoken({
token: localStorage.getItem("token"),
id: localStorage.getItem("id"),
})
);
}, []);
useEffect(() => {
if (isError) {
dispatch(clearState());
navigate("/login");
}
}, [isError]);
const handleOnCollapse = () => {
setCollapsed((prevState) => !prevState);
};
if (isFetching) {
return <div>Loading</div>;
}
if (user.id === "") {
return <Redirect noThrow to="/login" />;
}
return (
<Layout>
<SiderMenu collapsed={collapsed} handleOnCollapse={handleOnCollapse} />
<Layout>
<LayoutBanner
collapsed={collapsed}
handleOnCollapse={handleOnCollapse}
/>
<Content>{children}</Content>
</Layout>
</Layout>
);
};
export default MainLayout;
The second question would be how to get to the same page you were before the login redirect after login.
Thanks
I am building a backend admin app, and it will not stay on the correct route/page when refreshing the browser.
When the user logs in, it redirects to /dashboard, updates the state with user info and access token, sets a refresh token on the server in a httponly cookie.
When the access token expires and a route gets a 401 it calls check refresh token and gets a new access token and updates state. All of this works great except on refresh.
I believe it may have something to do with the HashRouter maybe? Not sure but I'm pulling out my hair because I'm sure it's something small I'm missing.
When visiting the page I've created a PrivateRoute for all traffic other than /login.
App.js
import React, {Component} from 'react';
import {HashRouter, Route, Switch} from 'react-router-dom';
import PrivateRoute from "./components/PrivateRoute";
import './scss/style.scss';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
)
// Pages
const Login = React.lazy(() => import('./views/pages/login/Login'));
// Containers
const TheLayout = React.lazy(() => import('./containers/TheLayout'));
class App extends Component {
render() {
return (
<HashRouter>
<React.Suspense fallback={loading}>
<Switch>
<Route exact path="/login" name="Login" render={props => <Login {...props}/>}/>
<PrivateRoute path="/" name="Home" render={props => <TheLayout {...props}/>}/>
</Switch>
</React.Suspense>
</HashRouter>
);
}
}
export default App;
In PrivateRoute is where I put the initial check whether there is the token and isLoggedIn state, if not it dispatches my refresh token function to update the state, which also functions properly, but still redirects to /login.
PrivateRoute.js
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {useDispatch, useSelector} from "react-redux";
import axios from "axios";
import {eraseToken, getRefreshedToken} from "../store/auth";
import store from "../store";
axios.defaults.withCredentials = true;
axios.interceptors.request.use(req => {
console.log(req)
const token = store.getState().auth.user.token;
if (token) {
req.headers.Authorization = 'Bearer ' + token;
} else {
//req.headers.Authorization = null;
}
return req;
});
axios.interceptors.response.use(res => {
return res;
}, async (error) => {
if (401 === error.response.status) {
if ('/refresh' !== error.response.config.url) {
await store.dispatch(getRefreshedToken());
} else {
store.dispatch(eraseToken());
}
} else {
return Promise.reject(error);
}
}
);
const PrivateRoute = ({render: Component, ...rest}) => {
const dispatch = useDispatch();
let token = useSelector(state => state.auth.user.token);
let isLoggedIn = useSelector(state => state.auth.isLoggedIn);
console.log(token, isLoggedIn);
if (!isLoggedIn || !token) {
dispatch(getRefreshedToken());
}
// check again after state update
token = useSelector(state => state.auth.user.token);
isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return (
<Route {...rest} render={props => (
(token && isLoggedIn)
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}}/>
)}/>
)
}
export default PrivateRoute;
This is where I'm confused. Once I log in I get redirected fine to /dashboard because token etc exists in state, however if I refresh the page while at /dashboard it calls dispatch(getRefreshedToken()); which works fine, and updates the state with the correct values, which should then just render the component (I assumed) instead of redirecting back to /login.
If I manually change the url and replace /login with /dashboard it views the dashboard fine still as the state exists properly.
The PrivateRoute renders a component called TheLayout, which renders a component called TheContent which maps routes to views to display.
routes.js
const routes = [
{ path: '/', exact: true, name: 'Home' },
{ path: '/dashboard', name: 'Dashboard', component: Dashboard },
{ path: '/users', name: 'Users', component: Users }
]
export default routes;
TheContent.js
import React, {Suspense} from 'react';
import {Redirect, Route, Switch} from 'react-router-dom';
import {CContainer, CFade} from '#coreui/react';
// routes config
import routes from '../routes';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
)
const TheContent = () => {
return (
<main className="c-main">
<CContainer fluid>
<Suspense fallback={loading}>
<Switch>
{routes.map((route, idx) => {
return route.component && (
<Route
key={idx}
path={route.path}
exact={route.exact}
name={route.name}
render={props => (
<CFade>
<route.component {...props} />
</CFade>
)}/>
)
})}
<Redirect from="/" to="/dashboard"/>
</Switch>
</Suspense>
</CContainer>
</main>
)
}
export default React.memo(TheContent);
Not sure if that might have something to do with it?
UPDATE
I realized that the code was not waiting on dispatch to finish updating the state, so I changed the PrivateRoute to instead use useEffect with dependencies, and got it working how I needed.
However, trying to understand useEffect better, it's firing my refresh and calling the server multiple times once my cookie expires. Is there any way to stop this from happening? Here is my updated Private Route.
const PrivateRoute = ({render: Component, ...rest}) => {
const dispatch = useDispatch();
const history = useHistory();
let token = useSelector(state => state.auth.user.token);
let isLoggedIn = useSelector(state => state.auth.isLoggedIn);
let isRefreshingToken = useSelector(state => state.auth.isRefreshingToken);
useEffect(() => {
if (!isLoggedIn && !token && !isRefreshingToken) {
(async () => {
await dispatch(getRefreshedToken())
.catch(err => {
history.push('/login');
});
})();
}
}, [token, isLoggedIn, isRefreshingToken, dispatch, history])
return (
<Route {...rest} render={props => (
<Component {...props} />
)}/>
)
}
PrivateRoute does not redirect to the login page it is still public. What is the problem?
this is the code...
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import AuthContext from '../context/auth/AuthContext';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated, loading } = useContext(AuthContext);
return (
<Route
{...rest}
render={props =>
!isAuthenticated && !loading ? (
<Redirect to='/login' />
) : (
<Component {...props} />
)
}
/>
);
};