What is the best way to make private route in reactjs - javascript

I made a private route in my app basically all the route in my app is private. So to make Private route I added a state isAuthenticated which is false by default but when user login in it becomes true. So on this isAuthenticated state I implemented a condition in private route component. But the problem with this is when user is logged in and refresh the page. I get redirect to / home page. I don't want that.
I am use token authentication here.
Here is my PrivateRoute.js
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
/>
);
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
};
}
export default connect(mapStateToProps)(PrivateRoute);

If all your routes when authenticated are private you can also just skip the autentication by route and use following
import PrivateApp from './PrivateApp'
import PublicApp from './PublicApp'
function App() {
// set isAuthenticated dynamically
const isAuthenticated = true
return isAuthenticated ? <PrivateApp /> : <PublicApp />
}
That way you do not need to think for each route if it is authenticated or not and can just define it once and depending on that it will render your private or public app. Within those apps you can then use the router normally and do not need to think who can access which route.

If you validate the token against a server (which you should) then it's an asynchronous operation and isAuthenticated will be falsy initially, causing the redirect on refresh.
You one way around this is an isAuthenticating state, e.g.
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ isAuthenticated, isAuthenticating, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if( isAuthenticating ){ return <Spinner /> }
return isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
}
/>
);
function mapStateToProps(state) {
return {
isAuthenticated: state.user.isAuthenticated,
isAuthenticating: state.authentication.inProgress
};
}
export default connect(mapStateToProps)(PrivateRoute);
isAuthenticating should be true by default then set to false when the server response is received and the user.isAuthenticated state is known.

Related

How to stop already logged in user to go back to the sign-in page in React.js

Here, I'm trying to stop the user to go back to the login page if the user is currently signed in, the first line in the render function is causing an error
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
But I'm not calling setState here, I'm just calling a function from another module to check if it returns a user or not
render() {
if (auth.getCurrentUser()) return <Redirect to="/" />;
return (
<div id="loginWrapper">
<h3>Login</h3>
<ToastContainer />
<form onSubmit={this.handleSubmit}>
{this.renderInput("username", "Username")}
{this.renderInput("password", "Password", "password")}
{this.renderButton("Login")}
</form>
</div>
);
}
and here is my auth module from where the function is being called,
import jwtDecode from "jwt-decode";
export function getCurrentUser() {
try {
const jwt = localStorage.getItem("token");
return jwtDecode(jwt);
} catch {
return null;
}
}
export default {
getCurrentUser,
};
use ternary condition with jwt token on your route which may help you to check the user is logged in or not also you can make your route private. ty
You could create a ProtectedRoute component that redirects based on auth status.
import React from 'react';
import { Route } from 'react-router-dom';
const ProtectedRoute = ({ component: Component, ...restProps }) => {
return (
<Route {...restProps} render={
props => {
if (!auth.getCurrentUser()) {
return <Component {...rest} {...props} />
} else {
return <Redirect to={
{
pathname: '/',
state: {
from: props.location
}
}
} />
}
}
} />
)
}
export default ProtectedRoute;
You can then call the ProtectedRoute component on the login page thus:
<ProtectedRoute path='/login' component={LoginPage} />

React-router: ProtectedRoute authentication logic not working

I am trying to implement protected routes that are only accessible after the user logs in.
Basically, there are two routes: /login Login component (public) and / Dashboard component (protected). After the user clicks on the Login button in /login, an API is called which returns an accessToken, which is then stored in localStorage. The protectedRoute HOC checks if the token is present in the localStorage or not and redirects the user accordingly.
After clicking on the Login button it just redirects back to the login page instead of taking user to Dashboard. Not sure what is wrong with the logic.
asyncLocalStorage is just a helper method for promise based localStorage operations.
App.js
import { useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Login from "./Login";
import ProtectedRoute from "./ProtectedRoute";
import { asyncLocalStorage } from "./asyncLocalStorage";
function App() {
const [auth, setAuth] = useState(false);
useEffect(() => {
const getToken = async () => {
const token = await asyncLocalStorage.getItem("accessToken");
if (token) setAuth(true);
};
getToken();
}, []);
return (
<div className="app">
<Router>
<Switch>
<ProtectedRoute exact path="/" isLoggedIn={auth} redirectTo="/login">
<Dashboard />
</ProtectedRoute>
<Route exact path="/login">
<Login />
</Route>
</Switch>
</Router>
</div>
);
}
export default App;
ProtectedRoute.js
import { Route, Redirect } from "react-router-dom";
const ProtectedRoute = ({ children, isLoggedIn, redirectTo, ...rest }) => {
return (
<Route
{...rest}
render={() => {
return isLoggedIn ? children : <Redirect to={redirectTo} />;
}}
/>
);
};
export default ProtectedRoute;
Dashboard.js
const Dashboard = () => {
return (
<div>
<h3>Dashboard</h3>
<button
onClick={() => {
localStorage.removeItem("accessToken");
window.location.href = "/login";
}}
>
Logout
</button>
</div>
);
};
export default Dashboard;
Login.js
import { asyncLocalStorage } from "./asyncLocalStorage";
const Login = () => {
return (
<div>
<h3>Login</h3>
<button
onClick={async () => {
// Making an API call here and storing the accessToken in localStorage.
await asyncLocalStorage.setItem(
"accessToken",
"SOME_TOKEN_FROM_API_RES"
);
window.location.href = "/";
}}
>
Login
</button>
</div>
);
};
export default Login;
asyncLocalStorage.js
export const asyncLocalStorage = {
setItem: (key, value) => {
Promise.resolve(localStorage.setItem(key, value));
},
getItem: (key) => {
return Promise.resolve(localStorage.getItem(key));
}
};
Problem: auth State Never Updated
Your ProtectedRoute component relies on the value of auth from the useState in App in order to determine whether the user is logged in or not. You set this value through a useEffect hook which runs once when the App is mounted. This hook is never run again and setAuth is never called anywhere else. If the user is logged out when App first mounts then the ProtectedRoute will always receive isLoggedIn={false} even after the user has logged in and the local storage has been set.
Solution: call setAuth from Login
A typical setup would check localStorage from the ProtectedRoute, but I don't think that will work with the async setup that you have here.
I propose that you pass both isLoggedIn and setAuth to the Login component.
<Login isLoggedIn={!!auth} setAuth={setAuth}/>
In the Login component, we redirect to home whenever we get a true value of isLoggedIn. If a users tries to go to "/login" when they are already logged in, they'll get redirected back. When the log in by clicking the button, they'll get redirected when the value of auth changes to true. So we need to change that value.
The onClick handler for the button will await setting the token in storage and then call setAuth(token). This will trigger the redirection.
import { asyncLocalStorage } from "./asyncLocalStorage";
import {Redirect} from "react-router-dom";
const Login = ({isLoggedIn, setAuth}) => {
if ( isLoggedIn ) {
return <Redirect to="/"/>
}
return (
<div>
<h3>Login</h3>
<button
onClick={async () => {
// Making an API call here and storing the accessToken in localStorage.
const token = "SOME_TOKEN_FROM_API_RES";
await asyncLocalStorage.setItem(
"accessToken",
token
);
setAuth(token);
}}
>
Login
</button>
</div>
);
};
export default Login;

how to access redux store from react component for authentication

I'm maintaining a react redux app and trying to get authentication to one of the routes in the app, namely /dashboard . I want to pass in a boolean state from redux store to a prop named authed but struggling... As currently, I just pass in true value as a fake value.
import React from 'react'
import {
HashRouter,
Route,
Link,
Switch,
Redirect
} from 'react-router-dom'
// components that are main pages
import Home from './containers/Home'
import Login from './containers/Login'
import Signup from './containers/Signup'
import NotFound from './containers/NotFound'
import Dashboard from './containers/Dashboard'
import IntersectionForm from './containers/IntersectionForm'
import IntersectionDetail from './containers/IntersectionDetail'
import { connect } from 'react-redux'
const PrivateRoute = ({component: Component, authed, ...rest}) => {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{pathname: '/', state: {from: props.location}}}/>}
/>
)
}
function mapStateToProps (state) {
return state
}
const PrivateRouteContainer = connect(mapStateToProps)(PrivateRoute)
const Routes = (history) => {
return (
<HashRouter history={history}>
<switch>
<Route exact path="/" component={Login}/>
<Route exact path="/signup" component={Signup}/>
<PrivateRouteContainer authed={true} path='/dashboard' component={Dashboard}/>
</switch>
</HashRouter>
)
}
export default Routes
Make a call to your auth end-point (POST) in auth_actions from your componentDidMount function.
dispatch an action once you get the response within actioncreator.
in authReducer - for example: isAuthenticated:true/false and return the payload.
access that value by making your react component connected and within
mapStatetoprops of the component and you can access this boolean
value - using this.props.authValue.
function mapStateToProps (state) {
return state
}
by doing this your component will receive props with all your states across reducers.
for example, if you have:
import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'
export default combineReducers({
todos,
counter
})
Then your PrivateRoute will get todos and counter props.
That's why its better if your mapStateToProps grabs just the prop it needs.
function mapStateToProps (state) {
return {
authed: state.nameOfReducer.isAuthed, // or whetever is the value you need to know if user is authorized
}
}
If you, however, don't combineReducers and you have just one reducer in your app then:
function mapStateToProps (state) {
return {
authed: state.isAuthed,
}
}

React router v4 use declarative Redirect without rendering the current component

I am using a similar code like this to redirect in my app after users logged in. The code looks like the following:
import React, { Component } from 'react'
import { Redirect } from 'react-router'
export default class LoginForm extends Component {
constructor () {
super();
this.state = {
fireRedirect: false
}
}
submitForm = (e) => {
e.preventDefault()
//if login success
this.setState({ fireRedirect: true })
}
render () {
const { from } = this.props.location.state || '/'
const { fireRedirect } = this.state
return (
<div>
<form onSubmit={this.submitForm}>
<button type="submit">Submit</button>
</form>
{fireRedirect && (
<Redirect to={from || '/home'}/>
)}
</div>
)
}
}
Works fine when a successful login has been triggered. But there is the case, that logged in users enter the login page and should be automatically redirected to the "home" page (or whatever other page).
How can I use the Redirect component without rendering the current component and without (as far as I understand discouraged) imperative pushing to the history (e.g. in componentWillMount)?
Solution 1
You could use withRouter HOC to access history via props.
Import withRouter.
import {
withRouter
} from 'react-router-dom';
Then wrap with HOC.
// Example code
export default withRouter(connect(...))(Component)
Now you can access this.props.history. For example use it with componentDidMount().
componentDidMount() {
const { history } = this.props;
if (this.props.authenticated) {
history.push('/private-route');
}
}
Solution 2 Much better
Here is example on reacttraining.
Which would perfectly work for you.
But you just need to create LoginRoute to handle problem you described.
const LoginRoute = ({ component: Component, ...rest }) => (
<Route
{...rest} render={props => (
fakeAuth.isAuthenticated ? (
<Redirect to={{
pathname: '/private-route',
state: { from: props.location }
}} />
) : (
<Component {...props} />
)
)} />
);
and inside <Router /> just replace
<Route path="/login" component={Login}/>
with
<LoginRoute path="/login" component={Login}/>
Now everytime somebody will try to access /login route as authenticated user, he will be redirected to /private-route. It's even better solution because it doesn't mount your LoginComponent if condition isn't met.
Here is another solution which doesn't touch React stuff at all. E.g. if you need to navigate inside redux-saga.
Have file history.js:
import {createBrowserHistory} from 'history';
export default createBrowserHistory();
Somewhere where you define routes, don't use browser router but just general <Router/>:
import history from 'utils/history';
...
<Router history={history}>
<Route path="/" component={App}/>
</Router>
That's it. Now you can use same history import and push new route.
In any part of your app:
import history from 'utils/history';
history.push('/foo');
In saga:
import {call} from 'redux-saga/effects';
import history from 'utils/history';
...
history.push('/foo');
yield call(history.push, '/foo');

Programmatically Navigate using react-router

I am developing an application in which I check if the user is not loggedIn. I have to display the login form, else dispatch an action that would change the route and load other component. Here is my code:
render() {
if (isLoggedIn) {
// dispatch an action to change the route
}
// return login component
<Login />
}
How can I achieve this as I cannot change states inside the render function.
Considering you are using react-router v4
Use your component with withRouter and use history.push from props to change the route. You need to make use of withRouter only when your component is not receiving the Router props, this may happen in cases when your component is a nested child of a component rendered by the Router and you haven't passed the Router props to it or when the component is not linked to the Router at all and is rendered as a separate component from the Routes.
import {withRouter} from 'react-router';
class App extends React.Component {
...
componenDidMount() {
// get isLoggedIn from localStorage or API call
if (isLoggedIn) {
// dispatch an action to change the route
this.props.history.push('/home');
}
}
render() {
// return login component
return <Login />
}
}
export default withRouter(App);
Important Note
If you are using withRouter to prevent updates from being blocked by
shouldComponentUpdate, it is important that withRouter wraps the
component that implements shouldComponentUpdate. For example, when
using Redux:
// This gets around shouldComponentUpdate
withRouter(connect(...)(MyComponent))
// This does not
connect(...)(withRouter(MyComponent))
or you could use Redirect
import {withRouter} from 'react-router';
class App extends React.Component {
...
render() {
if(isLoggedIn) {
return <Redirect to="/home"/>
}
// return login component
return <Login />
}
}
With react-router v2 or react-router v3, you can make use of context to dynamically change the route like
class App extends React.Component {
...
render() {
if (isLoggedIn) {
// dispatch an action to change the route
this.context.router.push('/home');
}
// return login component
return <Login />
}
}
App.contextTypes = {
router: React.PropTypes.object.isRequired
}
export default App;
or use
import { browserHistory } from 'react-router';
browserHistory.push('/some/path');
In react-router version 4:
import React from 'react'
import { BrowserRouter as Router, Route, Redirect} from 'react-router-dom'
const Example = () => (
if (isLoggedIn) {
<OtherComponent />
} else {
<Router>
<Redirect push to="/login" />
<Route path="/login" component={Login}/>
</Router>
}
)
const Login = () => (
<h1>Form Components</h1>
...
)
export default Example;
Another alternative is to handle this using Thunk-style asynchronous actions (which are safe/allowed to have side-effects).
If you use Thunk, you can inject the same history object into both your <Router> component and Thunk actions using thunk.withExtraArgument, like this:
import React from 'react'
import { BrowserRouter as Router, Route, Redirect} from 'react-router-dom'
import { createBrowserHistory } from "history"
import { applyMiddleware, createStore } from "redux"
import thunk from "redux-thunk"
const history = createBrowserHistory()
const middlewares = applyMiddleware(thunk.withExtraArgument({history}))
const store = createStore(appReducer, middlewares)
render(
<Provider store={store}
<Router history={history}>
<Route path="*" component={CatchAll} />
</Router
</Provider>,
appDiv)
Then in your action-creators, you will have a history instance that is safe to use with ReactRouter, so you can just trigger a regular Redux event if you're not logged in:
// meanwhile... in action-creators.js
export const notLoggedIn = () => {
return (dispatch, getState, {history}) => {
history.push(`/login`)
}
}
Another advantage of this is that the url is easier to handle, now, so we can put redirect info on the query string, etc.
You can try still doing this check in your Render methods, but if it causes problems, you might consider doing it in componentDidMount, or elsewhere in the lifecycle (although also I understand the desire to stick with Stateless Functional Compeonents!)
You can still use Redux and mapDispatchToProps to inject the action creator into your comptonent, so your component is still only loosely connected to Redux.
This is my handle loggedIn. react-router v4
PrivateRoute is allow enter path if user is loggedIn and save the token to localStorge
function PrivateRoute({ component: Component, ...rest }) {
return (
<Route
{...rest}
render={props => (localStorage.token) ? <Component {...props} /> : (
<Redirect
to={{
pathname: '/signin',
state: { from: props.location },
}}
/>
)
}
/>
);
}
Define all paths in your app in here
export default (
<main>
<Switch>
<Route exact path="/signin" component={SignIn} />
<Route exact path="/signup" component={SignUp} />
<PrivateRoute path="/" component={Home} />
</Switch>
</main>
);
Those who are facing issues in implementing this on react-router v4. Here is a working solution for navigating through the react app programmatically.
history.js
import createHistory from 'history/createBrowserHistory'
export default createHistory()
App.js OR Route.jsx. Pass history as a prop to your Router.
import { Router, Route } from 'react-router-dom'
import history from './history'
...
<Router history={history}>
<Route path="/test" component={Test}/>
</Router>
You can use push() to navigate.
import history from './history'
...
render() {
if (isLoggedIn) {
history.push('/test') // this should change the url and re-render Test component
}
// return login component
<Login />
}
All thanks to this comment: https://github.com/ReactTraining/react-router/issues/3498#issuecomment-301057248
render(){
return (
<div>
{ this.props.redirect ? <Redirect to="/" /> :'' }
<div>
add here component codes
</div>
</div>
);
}
I would suggest you to use connected-react-router https://github.com/supasate/connected-react-router
which helps to perform navigation even from reducers/actions if you want.
it is well documented and easy to configure
I was able to use history within stateless functional component, using withRouter following way (needed to ignore typescript warning):
import { withRouter } from 'react-router-dom';
...
type Props = { myProp: boolean };
// #ts-ignore
export const MyComponent: FC<Props> = withRouter(({ myProp, history }) => {
...
})
import { useNavigate } from "react-router-dom"; //with v6
export default function Component() {
const navigate = useNavigate();
navigate.push('/path');
}
I had this issue and just solved it with the new useNavigate hook in version 6 of react-router-dom

Categories

Resources