Reputation: 499
I am using react, redux, react-router and higher order components. I have some protected routes that the user needs to be logged in to view and I have wrapped these inside a higher order component, I will try to give as clear a context as possible so bear with me. Here is my Higher Order Component (HOC):
import React from 'react';
import {connect} from 'react-redux';
import { browserHistory } from 'react-router';
export function requireAuthentication(Component, redirect) {
class AuthenticatedComponent extends React.Component {
componentWillMount() {
this.checkAuth();
}
componentWillReceiveProps(nextProps) {
this.checkAuth();
}
checkAuth() {
if (!this.props.isAuthenticated) {
browserHistory.push(redirect);
}
}
render() {
console.log('requireAuthentication render');
return (
<div>
{this.props.isAuthenticated === true
? <Component {...this.props}/>
: null
}
</div>
)
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.loggedIn
});
return connect(mapStateToProps)(AuthenticatedComponent);
}
The HOC makes a check to the redux store to see whether the user is logged in and it is able to do this because the returned Auth compononent is connected to the redux store.
And this is how I am using the HOC in my routes with react router:
import React, { Component } from 'react';
import { Router, Route, browserHistory, IndexRoute } from 'react-router';
import Nav from './nav';
import MainBody from './main-body';
import Login from './login';
import Signup from '../containers/signup';
import Dashboard from './dashboard';
import { requireAuthentication } from './require-authentication';
export default class Everything extends Component {
render() {
return (
<div>
<a className="iconav-brand intelliagg-logo-container" href="/">
<span className="iconav-brand-icon intelliagg">Deep-Light</span>
</a>
<Router history={browserHistory}>
<Route path='/' component={Login} >
</Route>
<Route path='/main' component={MainBody}>
<Route path='/dashboard' component={ requireAuthentication(Dashboard, '/') } />
<Route path='/signup' component={Signup} />
</Route>
</Router>
</div>
);
}
}
I import the HOC function and I pass in the protected route to HOC, which in this case is Dashboard. If the user is not logged in and tries to access this view, they will be redirected to the login page.
When the user initiates a login this is done with an action creator that makes a request to the server with the username and password provided by the user and I have a reducer that updates the state with a flag that says isAuthenticated: true
:
import { browserHistory } from 'react-router';
export default function(state = false, action) {
let newState;
switch (action.type) {
case 'LOGGED_IN':
newState = action.payload.data.login_status;
return newState;
break;
default:
return state;
}
}
my reducers:
import { combineReducers } from 'redux';
import formFields from './reducer_form-fields';
import hintReducer from './reducer_hint-reducer';
import errorMessage from './reducer_error-message-submission';
import loggedIn from './reducer_logged-in';
import isAuthenticated from './reducer_isAuthenticated';
const rootReducer = combineReducers({
formInput: formFields,
hint: hintReducer,
errorMessage,
loggedIn,
isAuthenticated
});
export default rootReducer;
So, my problem is that the HOC component is always checking the isAuthenticated flag and if that is set to false, which is the default, it will redirect the user to the login page. Since the request to login takes some time and for the state to be updated, it means that the user will always be redirected to the login page since the check is immediate on the client side from the HOC to the redux store.
How can I do the check and redirect the user accordingly based on the response from the server? Is there a way to delay the checking of the redux value in the store from the HOC?
Upvotes: 3
Views: 1362
Reputation: 615
I disagree with Hayo in that using onEnter would be the best approach. See this SO post on why I think a HOC solution (like you use) is a better approach: Universal Auth Redux & React Router.
As for your specific question, you can accomplish this by adding an isAuthenticating
boolean to your reducer and passing that into your HOC during the checkAuth()
function. When it is true, you probably want to display an alternative Component like a loading bar/spinner/etc to provide some feedback to the user. Your reducer can default this bool to true and then set it to false on LOGGED_IN
. Your reducer might look something like this:
const initialState = { isAuthenticating: true, isAuthenticated: false };
export default function(state = initialState, action) {
let newState;
switch (action.type) {
case 'LOGGED_IN':
newState = { isAuthenticating: false, isAuthenticated: action.payload.data.login_status };
return newState;
default:
return state;
}
}
Then in the render()
of your HOC you can return a LoadingComponent
instead of null if isAuthenticating
is true.
This is pretty similar to feature we just added to redux-auth-wrapper in this PR - https://github.com/mjrussell/redux-auth-wrapper/pull/35 if you want to use that for more reference
Upvotes: 6
Reputation: 149
I think you should solve the problem by configuring a dynamic route with react-router. Your solution tries to solve the problem within the react component and not in the react-router.
In other words, in case of isAuthenicated is false, you go to route '/'. In case the isAuthenicated is true the route goes to the dashboard or whatever is requested.
Take a look at the end of the page in Configuration with Plain Routes and getComponent fucntions. React-Router has also a onEnter attribute which could look similar to following in your case:
module.exports = store => ({
component: require('./main.jsx'),
childRoutes: [
// public routes
{ getComponents(nextState, cb) {
return require.ensure([], (require) => {
cb(null, require('./login'))
})
},{
...
},{
onEnter: (nextState, replace) => {
const { isAuthenticated } = store.getState();
if (!isAuthenticated) {
replace('/');
}
},
// Protected routes
childRoutes: [... ]
},{
// Share the path (magic is here)
// Dynamically load the correct component
path: '/',
getComponent: (location, cb) => {
const { isAuthenticated } = store.getState();
if (isAuthenticated)
return require.ensure([], (require) => {
cb(null, require('./dashboard'))
})
}
return require.ensure([], (require) => {
cb(null, require('./signup'))
})
}
}
I hope this helps and gives an idea to solve the problem.
Upvotes: 0