This is my wrapper component aka auth component to allow only authed user to access.
Auth.js
render() {
const { Component: component, ...rest } = this.props;
return (
<Route>
{rest}
render={props =>
this.isAuth === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login"
}}
/>
)
}
</Route>
);
}
What's wrong with it? Here's my route declaration
Index.js
render(
<BrowserRouter>
<div className="App">
<Switch>
<Route path="/login" component={Login} />
<Auth path="/dashboard" component={Dashboard} />
</Switch>
</div>
</BrowserRouter>,
document.getElementById("root")
);
I've made a demo to reproduce the problem here
https://codesandbox.io/s/5xm202p1j4
I used the same concept in the official doc, https://reacttraining.com/react-router/web/example/auth-workflow just that instead of making the auth.js a pure function I make it a class base container so that I can call api or check the token of the user exist or not in my localstorage, what's the problem exactly?
Did you mean to do something like this:
render() {
const { Component: component, ...rest } = this.props;
return (
<Route
{...rest}
render={props =>
this.isAuth === true ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login"
}}
/>
)
}
/>
);
}
By having rest and render in the <Route> tag so the rest are passed to the <Route>?
Related
Hi guys i have a problem. When I enter "dashboard" which is a private route it redirects me to "login" first then to dashboard. True and False are playing together. How can i fix it to not redirect me to login then to dashboard.
video example:
https://cdn.aboutluc.xyz/images/rc64kb6af92sswn3r4mv.mp4
code:
import React, {
useState,
useEffect
} from "react"
import { toast } from "react-toastify"
import {
BrowserRouter as Router,
Routes,
Route,
Navigate
} from "react-router-dom"
import {
Login,
Register,
Dashboard,
} from "./Pages"
import {
Navbar
} from "./Components"
import './App.css'
import "react-toastify/dist/ReactToastify.css"
import 'bootstrap/dist/css/bootstrap.min.css'
toast.configure()
const App = () => {
const [ isAuthenticated, setIsAuthenticated ] = useState()
const setAuth = (boolean) => {
setIsAuthenticated(boolean)
}
const isAuth = async () => {
try {
const res = await fetch("http://localhost:5000/api/auth/verify", {
headers: { JwtToken: localStorage.JwtToken }
});
const parseRes = await res.json();
parseRes === true ? setIsAuthenticated(true) : setIsAuthenticated(false);
} catch (error) {
console.error(error)
}
}
useEffect(() => {
isAuth()
}, [])
return (
<>
<Router>
<Navbar setAuth={setAuth} isAuthenticated={isAuthenticated} />
<Routes>
<Route
exact
path="/login"
element={
isAuthenticated ? (
<Navigate replace={true} to="/dashboard" />
) : (
<Login setAuth={setAuth} />
)
}
/>
<Route
exact
path="/register"
element={
isAuthenticated ? (
<Navigate replace={true} to="/dashboard" />
) : (
<Register setAuth={setAuth} />
)
}
/>
<Route
exact
path="/dashboard"
element={
isAuthenticated ? (
<Dashboard setAuth={setAuth} />
) : (
<Navigate replace={true} to="/login" />
)
}
/>
</Routes>
</Router>
</>
)
}
export default App
The possible issue I see is the "gap" on the initial render where the isAuthenticated state is undefined and the useEffect hook callback to set that state hasn't run yet. If you attempt to directly access a protected route then regardless of actual auth status the code will bounce you to the login route.
For this you typically want to use the "third" indeterminant state to "hold" on either redirecting to auth or allowing access through to the protected component until the auth status is confirmed.
Abstract the auth status into auth layout components.
const AuthLayout = ({ isAuthenticated }) => {
if (isAuthenticated === undefined) return null; // or loading spinner, etc...
return isAuthenticated
? <Outlet />
: <Navigate to="/login" replace />;
};
const AnonymousLayout = ({ isAuthenticated, to }) => {
if (isAuthenticated === undefined) return null; // or loading spinner, etc...
return isAuthenticated
? <Navigate to={to} replace />
: <Outlet />;
};
User the layouts to guard/protect specific routes.
<Routes>
<Route
element={(
<AnonymousLayout isAuthenticated={isAuthenticated} to="/dashboard" />
)}
>
<Route path="/login" element={<Login setAuth={setAuth} />} />
<Route path="/register" element={<Register setAuth={setAuth} />} />
</Route>
<Route element={<AuthLayout isAuthenticated={isAuthenticated} />}>
<Route path="/dashboard" element={<Dashboard setAuth={setAuth} />} />
</Route>
</Routes>
Note: You only ever call isAuth when the App component mounts. You may want to call this function or otherwise validate your auth token a little more often than this. Passing isAuth into the route wrappers and invoking also in an useEffect hook probably isn't a terrible idea.
Currently using react-router-dom 6.1.1 and I'm working with a private route.
Inside this private route I usually had other routes (so that I can keep my Sidebar on them).
My code looks like this
// App.tsx
const RequireAuth: React.FC<PrivateRouteProps> = ({ children, redirectTo }) => {
const isAuthenticated = Auth.isLogedIn()
return isAuthenticated ? children : <Navigate to={redirectTo} />
}
const Page = () => {
return (
<div className={css.host}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<RequireAuth redirectTo="/login">
<Home />
</RequireAuth>
}
/>
</Routes>
</BrowserRouter>
</div>
)
}
// Home/index.tsx
const Home = () => {
return (
<div className={css.host}>
<Sidebar sections={sidebarOptions(t)} />
<Routes>
{routes.map(({ breadCrumbtitle, link, component }, index) => (
<Route path={link} key={index}>
{component ? component : <p>[{breadCrumbtitle}] To be done</p>}
</Route>
))}
</Routes>
</div>
)
}
So... This setup worked with v5 but it seems to be something that doesn't really work with v6.
What can I do if I still want to keep the Sidebar for all the routes once I'm logged in?
I ended up finding the solution to my issue.
Doing what Drew Reese suggested only worked to a certain point since I was being led to a route that, for react router, didn't exist.
For it to work I add to do
// App.tsx
const RequireAuth: React.FC<PrivateRouteProps> = ({ children, redirectTo }) => {
const isAuthenticated = Auth.isLogedIn()
return isAuthenticated ? children : <Navigate to={redirectTo} />
}
const Page = () => {
return (
<div className={css.host}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path=""
element={
<RequireAuth redirectTo="/login">
<Home />
</RequireAuth>
}
>
{routes.map(({ breadCrumbtitle, link, component }, index) => {
return <Route path={link} key={index} element={component}></Route>
})}
</Route>
</Routes>
</BrowserRouter>
</div>
)
}
// Home/index.tsx
const Home = () => {
return (
<div className={css.host}>
<Sidebar sections={sidebarOptions(t)} />
<div className={css.contentContainer}>
<Outlet />
</div>
</div>
)
}
Using the Outlet seemed to be essential, don't know if it's something new on react router v6 but seemed to do the trick!
As far as I can tell the only issue is with the routes mapping, the Route components have invalid children, i.e. you are rendering another Route or React.Fragment as a child.
Move this up to the element prop of the mapped Route components.
const Home = () => {
return (
<div className={css.host}>
<Sidebar sections={sidebarOptions(t)} />
<Routes>
{routes.map(({ breadCrumbtitle, link, component }, index) => (
<Route
path={link}
key={index}
element={component || <p>[{breadCrumbtitle}] To be done</p>}
/>
))}
</Routes>
</div>
);
};
React app with react-router-dom: 4.3.1:
Main App.js render:
render() {
let routes = (
<Switch>
<Route component={LogIn} path="/login" />
<Redirect to="/login" />
</Switch>
);
if (this.props.isAuthenticated) {
routes = (
<Switch>
<Route component={ParcelListView} path="/" exact />
<Route component={StatusTable} path="/status" />
<Redirect to="/" />
</Switch>
);
}
return (
<div className="app">
{routes}
</div>
);
}
I see white screen When use this code, but when I assign to routes first or second Switch without if it works perfectly in both cases.
I guess the problem comes from assignment in if block. Is this some kind of async thing?
You might want to set routes inside of a <Switch /> component whatever the scenario and have either public or private route components. Here is a common approach:
const PublicRoute = ({
isAuthenticated,
component: Component,
...rest
}) => (
<Route
{...rest}
component={props => (
isAuthenticated ? (
<Redirect to="/somewhere" />
) : (
<Component {...props} />
))}
/>
);
const PrivateRoute = ({
isAuthenticated,
component: Component,
...rest
}) => (
<Route
{...rest}
component={props => (
isAuthenticated ? (
<div>
<Header />
<Component {...props} />
</div>
) : (
<Redirect to="/login" />
)
)}
/>
);
Both components take component (function) and isAuthenticated(boolean) as props and we pass the rest of the props down ({...rest}) anyway (path etc.)
This way you're able to allow/deny routes based on the propspassed down to your components:
...your code
render() {
<Switch>
<PublicRoute path="/" component={YourPublicComponent} />
<PrivateRoute path="/" isAuthenticated component={ParcelListView} />
</Switch>
}
More at Tyler McGinnis's website: https://tylermcginnis.com/react-router-protected-routes-authentication/
Another post on the subject: https://medium.com/#tomlarge/private-routes-with-react-router-dom-28e9f40c7146
You'll be able to find a lot of stuff on the subject on the web
I have this piece of code in my app.js and have configured a PrivateRoute which requires the user to login and only allows access if the cookie is set. However, I would like to restrict users trying to hit /login after they have successfully logged in. I used the reverse logic of the PrivateRoute and created LoginRoute which serves the purpose but would like to know if there is a better approach.
import React from 'react';
import {
BrowserRouter as Router,
Route,
Switch,
Redirect
} from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router'
import cookies from 'cookies-js';
import Home from './homeComponent';
import Login from './loginComponent';
import Dashboard from './dashboardComponent';
import NoMatch from './noMatchComponent';
const App = ({ history }) => {
return (
<ConnectedRouter history={history}>
<Switch>
<Route exact={true} path="/" component={Home} />
<LoginRoute path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
<Route component={NoMatch} />
</Switch>
</ConnectedRouter>
);
};
const LoginRoute = ({ component: Component, rest }) => (
<Route {...rest} render={(props) => (
cookies.get('access-token')
? <Redirect to={{
pathname: '/dashboard',
state: { from: props.location }
}} />
: <Component {...props} />
)} />
)
const PrivateRoute = ({ component: Component, rest }) => (
<Route {...rest} render={(props) => (
cookies.get('access-token')
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
export default App;
There are a few ways to handle Private Routes, one such way is to write a custom Login route as you have written which prevents user from visiting /login if he/she is already loggedIn. The only correction that you need to make in your route is to use rest syntax correctly
const LoginRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
cookies.get('access-token')
? <Redirect to={{
pathname: '/dashboard',
state: { from: props.location }
}} />
: <Component {...props} />
)} />
)
and PrivateRoute
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
cookies.get('access-token')
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
The other way to handle this would be an authentication HOC.
const RequireAuth = (Component) => {
return class App extends Component {
render() {
const { location } = this.props;
if (cookies.get('access-token')) {
if(location.pathname === '/login') {
return <Redirect to={'/dashboard'} />
}
return <Component {...this.props} />
}
return <Redirect to="/login"/>
}
}
}
export { RequireAuth }
and you would use it like
const App = ({ history }) => {
return (
<ConnectedRouter history={history}>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/login" component={RequireAuth(Login)} />
<Route path="/dashboard" component={RequireAuth(Dashboard)} />
<Route component={NoMatch} />
</Switch>
</ConnectedRouter>
);
};
This is based almost entirely on the React-router basic auth example. The only difference is that the user data comes from redux with mapStateToProps. So why is the initial this.props.isLoggedIn simply empty?
const PrivateRoute = ({ component: Component, userLoggedIn, ...rest }) => (
<Route {...rest} render={props => (
userLoggedIn ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}}/>
)
)}/>
)
class App extends Component {
componentWillMount () {
console.log(this.props.isLoggedIn) // getting correct value
}
render () {
return (
<Router>
<div>
{this.props.isLoggedIn} // <============= EMPTY :(
<Route exact path="/" render={() => (
!this.props.isLoggedIn ? (
<Redirect to="/login"/>
) : (
<Redirect to="/studio"/>
)
)}/>
<Route path="/login" component={Login}/>
<PrivateRoute
path="/studio"
component={Studio}
userLoggedIn={this.props.isLoggedIn}
/>
</div>
</Router>
)
}
}
const mapStateToProps = state => ({
isLoggedIn: state.user.isLoggedIn,
})