Ad

Modal Dialog Auth With React-router

I have react/redux/react-router application with public and private parts. Login and register form are shown as modal dialogs and don't have their own routes.

Desired flow: user clicks on link -> modal dialog is shown on current page -> in case of successful auth transition to linked page, else leave user on current page.

If there is no current page-show index page and continue with the flow

I'm tried to archive this using onEnter hook, but as far as i can see transition happens before hook is executed. If I try to use history.goBack() it's causes rerendering of page and looks nasty.

Is there any way to solve this problem without unnecessary redirects and extra render calls?

Ad

Answer

OK - I think I came up with a way to handle this that covers all the corner cases. It does require that you have some way to access application state from almost any component though. I am using Redux for that. This also assumes that login, register, etc do NOT have routes.

What I did was create two 'wrapper' components. The first one wraps any insecure routes and stores the location to a state value so that we always have a reference to the last insecure route...

import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { setRoute } from './redux/client/ducks/auth';

function mapDispatchToProps(dispatch) {
  return {
    setRoute: bindActionCreators(setRoute, dispatch)
  };
}

@connect(null, mapDispatchToProps)
export default class InsecureWrapper extends Component {

  componentDidMount() {
    const { location, setRoute } = this.props;
    setRoute(location.pathname);
  }

  render() {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}

The other one wraps all secure routes. It displays the login dialog (can also toggle back and forth between login and register or whatever) and prevents the content from being displayed unless logged into the app...

import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as authActions from './redux/client/ducks/auth';

function mapStateToProps(state) {
  return {
    auth: state.auth
  };
}

function mapDispatchToProps(dispatch) {
  return {
    authActions: bindActionCreators(authActions, dispatch)
  };
}

@connect(
  mapStateToProps,
  mapDispatchToProps
)
export default class SecureWrapper extends Component {

  componentDidMount() {
    const { auth, authActions } = this.props;
    //see if user and if not prompt for login
    if(!auth.loggedIn) {
      authActions.openLogin();
    }
  }

  //close any open dialogs in case the user hit browser back or whatever 
  componentWillUnmount() {
    const { authActions } = this.props;
    authActions.resetAuthDialogs();
  }    

  render() {
    const { auth, children } = this.props;
    return (
        <div className="container">
          {auth.loggedIn &&
            {children} ||
            <span>YOU MUST BE LOGGED IN TO VIEW THIS AREA!</span>
          }
        </div>
    );
  }

}

Then in the routes, just wrap them in the wrappers as needed...

import App from './containers/App';
import Dashboard from './containers/Dashboard';
import Secure from './containers/Secure';
import AuthWrapper from './common/client/components/AuthWrapper';
import InsecureWrapper from './common/client/components/InsecureWrapper';

export default [
  {path: '/', component: App, //common header or whatever can go here
  childRoutes: [
    {component: InsecureWrapper,
      childRoutes: [ //<- ***INSECURE ROUTES ARE CHILDREN HERE***
        {path: 'dashboard', component: Dashboard}
      ]
    },
    {component: SecureWrapper,
      //***SECURE ROUTES ARE CHILDREN HERE***
      childRoutes: [
        {path: 'secure', component:Secure}
      ]
    }
  ]}
]

Last but not least... in your Dialogs, you need to handle the cancel by pushing (or replacing) the location to the saved state value. And of course on successful login, just close them...

import { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as authActions from './redux/client/ducks/auth';
import LoginDialog from './common/client/components/dialogs/LoginDialog';
import RegisterDialog from './common/client/components/dialogs/RegisterDialog';
// this could also be replacePath if you wanted to overwrite the history
import { pushPath } from 'redux-simple-router'; 

function mapStateToProps(state) {
  return {
    auth: state.auth
  };
}

function mapDispatchToProps(dispatch) {
  return {
    authActions: bindActionCreators(authActions, dispatch),
    pushPath: bindActionCreators(pushPath, dispatch),
  };
}

@connect(
  mapStateToProps,
  mapDispatchToProps
)
export default class AuthContainer extends Component {

  _handleLoginCancel = (e) => {
    const { auth, authActions, pushPath } = this.props;
    pushPath(auth.prevRoute); // from our saved state value
    authActions.closeLogin();
  };

  _handleLoginSubmit = (e) => {
    const { authActions } = this.props;
    // do whatever you need to login here
    authActions.closeLogin();
  };

  render() {
    const { auth } = this.props;
    return (
      <div>
        <LoginDialog
          open={auth.showLogin}
          handleCancel={this._handleLoginCancel}
          handleSubmit={this._handleLoginSubmit}
          submitLabel="Login"
        />
       ...
      </div>
    )
  }
}

I am obviously using ES6, Babel, and webpack... but the principles should apply without them as should not using Redux (you could store the prev route in local storage or something). Also I left out some of the intermediate components that pass props down for brevity.

Some of these could be functional components, but I left them full to show more detail. There is also some room for improvement by abstracting some of this, but again I left it redundant to show more detail. Hope this helps!

Ad
source: stackoverflow.com
Ad