Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On React Router, how to stay logged in state even page refresh?

I'm making a website with React, React Router, and Redux. Lots of routes (pages) require users to be logged in. I can redirect to the login page if the user is not logged in like this:

function requireAuth(nextState, replace) {     let loggedIn = store.getState().AppReducer.UserReducer.loggedIn;      if(!loggedIn) {         replace({             pathname: '/login',             state: {                 nextpathname: nextState.location.pathname             }         });     } }  ReactDOM.render(     <Provider store={store}>         <Router history={history}>             <Route path="/" component={App}>                 <IndexRoute component={Index} />                 <Route path="login" component={Login} />                 <Route path="register" component={Register} />                 <Route path="dashboard" component={Graph} onEnter={requireAuth}>                     ... some other route requires logged in ...                 </Route>             </Route>         </Router>     </Provider>,     document.getElementById('entry') ); 

Please see the code, I used the onEnter hook to redirect to the '/login' route if the user is not logged in. Data for checking if the user is logged in is in the store and it will update after the user logs in.

It's working perfectly, but the problem is when I refresh the page, the store is reset and the user is not logged in state back.

I know this happens because the Redux store is just memory storage, so refreshing the page will lose all data from the store.

Checking the server session on every refresh may work but this might be too many requests, so that seems like a bad idea.

Saving the logged in state data to localStorage might work, but in this case, I should check every AJAX calls fail that request rejected because session is expired or not exists like something, and that seems like a bad idea too.

Is there a way to solve this problem more simply? My website needs to handle lots of users so I want to reduce XHR calls as much as possible.

Any advice will be very appreciated.

like image 536
modernator Avatar asked Aug 23 '16 09:08

modernator


People also ask

Does React router refresh the page?

react-router-dom allows us to navigate through different pages on our app with/without refreshing the entire component. By default, BrowserRouter in react-router-dom will not refresh the entire page.

How do you refresh a page without reloading in React?

Method 1: Refresh a Page Using JavaScriptwindow. location. reload(false); This method takes an optional parameter which by default is set to false.


1 Answers

Another way to go is to use JSON Web Tokens (JWT) that are required for each route, and localStorage to check for the JWT.

TL;DR

  • On the front end you have a signin and signup route that queries your server for a JWT according to the authentication on the server. Once passed the appropriate JWT you would then set a property of state to true. You can have a signout route that allows the user to set this state to false.

  • The index.js which contains your routes can check local storage before rendering, thus eliminating your problem with losing the state on refresh but keeping some security.

  • All routes requiring authentication in your application are rendered through a Composed Component, and secured with the necessity of having JWTs in the header for authorization on the server API.

Setting this up takes a little time but it will make your application 'reasonably' secure.


To solve your problem:

Check the local storage before the routes in your index.js file as shown below, updating the state to authenticated if required.

The application maintains security with the fact that the API is secured by the JWT which would solve your refresh issue, and maintain a secure link to your server and data.

Thus in the routes you would have something like this:

index.js

import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware, compose } from 'redux'; import { Router, Route, browserHistory, IndexRoute } from 'react-router'; import reduxThunk from 'redux-thunk'; import { AUTHENTICATE_THE_USER } from './actions/types'; import RequireAuth from './components/auth/require_auth'; import reducers from './reducers';  /* ...import necessary components */  const createStoreWithMiddleware = compose(applyMiddleware(reduxThunk))(createStore);  const store = createStoreWithMiddleware(reducers);  /* ... */  // Check for token and update application state if required const token = localStorage.getItem('token'); if (token) {     store.dispatch({ type: AUTHENTICATE_THE_USER }); }  /* ... */  ReactDOM.render(   <Provider store={store}>     <Router history={history}>       <Route path="/" component={App}>         <IndexRoute component={Index} />         <Route path="login" component={Login} />         <Route path="register" component={Register} />         <Route path="dashboard" component={RequireAuth(Graph)} />         <Route path="isauthenticated" component={RequireAuth(IsAuthenticated)} />         ... some other route requires logged in ...       </Route>     </Router>   </Provider>   , document.getElementById('entry')); 

RequiredAuth is the composed component while Graph and IsAuthenticated (can be any number of appropriately named components) require the state.authenticated to be true.

The Components, in this case Graph and IsAuthenticated rendered if the state.authenticated is true. Otherwise is defaults back to the root route.


Then you could build a Composed Component like this, through which all your routes are rendered. It will check that the state in which you are holding whether or not the user is authenticated (a boolean) is true before rendering.

require_auth.js

import React, { Component } from 'react'; import { connect } from 'react-redux';  export default function (ComposedComponent) {    // If user not authenticated render out to root    class Authentication extends Component {     static contextTypes = {       router: React.PropTypes.object     };      componentWillMount() {       if (!this.props.authenticated) {         this.context.router.push('/');       }     }      componentWillUpdate(nextProps) {       if (!nextProps.authenticated) {         this.context.router.push('/');       }     }      render() {       return <ComposedComponent {...this.props} />;     }   }    function mapStateToProps(state) {     return { authenticated: state.authenticated };   }    return connect(mapStateToProps)(Authentication); } 

On the signup/signin side you could create an action that stores the JWT and sets up the state to authenticated through an action-creator -> redux store. This example makes use of axios to run the async HTTP request response cycle.

export function signinUser({ email, password }) {    // Note using the npm package 'redux-thunk'   // giving direct access to the dispatch method   return function (dispatch) {      // Submit email and password to server     axios.post(`${API_URL}/signin`, { email, password })       .then(response => {         // If request is good update state - user is authenticated         dispatch({ type: AUTHENTICATE_THE_USER });          // - Save the JWT in localStorage         localStorage.setItem('token', response.data.token);          // - redirect to the route '/isauthenticated'         browserHistory.push('/isauthenticated');       })       .catch(() => {         // If request is bad show an error to the user         dispatch(authenticationError('Incorrect email or password!'));       });   }; }  

You would also need to set up your store (Redux in this case) and action creator of course.

The 'real' security comes from the back end. And to do this you use localStorage to keep the JWT on the front end and pass it in the header to any API calls that have sensitive/protected information.

Creating and parsing the JWT for users on the server API is another step. I have found passport to be effective.

like image 101
alex Avatar answered Sep 29 '22 09:09

alex