React-router: onEnter callbacks, transition blocking and async

Created on 26 Jul 2016  路  5Comments  路  Source: ReactTraining/react-router

react-router 2.6.0

The documentation for onEnter states:

If callback is listed as a 3rd argument, this hook will run asynchronously, and the transition will block until callback is called.

That does not seem to be the case. Here is a method I am using as an onEnter.

  requireAuth(nextState, replace, callback) {
    const { store } = this.props;
    const { getState, dispatch } = store;

    if (!isLoggedIn(getState())) {

      dispatch(hasSession())
        .catch((error) => {
          console.log('Replace');
          replace('/login');
          //callback();
        });

    } else {
      console.log('OK');
      //callback();
    }
  }

In this scenario, the documentation indicates that the transition will block until callback() is called. That would indicate to me that without calling callback(), the route will not transition. However, the above will transition to the route, the replace is ignored (if that is the path taken).

Comment in those callbacks

  requireAuth(nextState, replace, callback) {
    const { store } = this.props;
    const { getState, dispatch } = store;

    if (!isLoggedIn(getState())) {

      dispatch(hasSession())
        .catch((error) => {
          console.log('Replace');
          replace('/login');
          callback();
        });

    } else {
      console.log('OK');
      callback();
    }
  }

and the result is a little more as expected. The replace is correctly made use of and we can end up on the /login route. However, the transition did not seem to be blocked as the component of the route was definitely displayed on screen before the transition to the /login route.

Conversely, if callback is not specified, it does not seem possible to use replace in Promise resolution methods.

  requireAuth(nextState, replace) {
    const { store } = this.props;
    const { getState, dispatch } = store;

    if (!isLoggedIn(getState())) {

      dispatch(hasSession())
        .catch((error) => {
          console.log('Replace');
          replace('/login');
        });

    } else {
      console.log('OK');
    }
  }

This replace will never cause the /login route to appear.

These seems like a bug to me, or at the very least there is a mistake/hole in the documentation.

Most helpful comment

@dpwrussell

I saw your post at 329. I had the same issue with hook.length always 0.

I got it to work by wrapping my requireAuth in a function:

<Route path="/" component={SecretStuffComponent} 
 onEnter={(nextState, replace, callback) => { this.requireAuth(nextState, replace, callback) }}>

That seems to keep the hook.length at 3.
My hot-loader build seems to work.

All 5 comments

We look at your hook function's arity (aka, number of arguments (aka, hookFunction.length)) to determine if we need to wait for the callback or not. Your first example doesn't work means you have 3 args, so the code is waiting on your callback to be run.

As for the display of your component, it's hard to see without knowing your route config or how you render things. I would suggest looking at the async auth example for structural pointers.

Yeah, this turns out not to be a react-router problem. This seems to be caused by hot-loading. https://github.com/gaearon/react-hot-loader/issues/329

All the issues I was seeing were related to the length of hook always returning zero and thus never being in callback mode.

@dpwrussell How is it you have access to this.props inside your onEnter function? Are you using a modified version?

@johnnyfreeman That is actually kinda where I ran into trouble, but not why! MyRouter class has this.props that I needed so I decided to make requireAuth a method of this class and then use it from the routes as this.requireAuth. That works just fine except that the Hot Loader bug means the reported length of the requireAuth arguments is misreported, causing callback to be ignored. The bug does not seem to occur if you simply use a function for requireAuth and then bind it (crucial, as this is what gives it access to this) into the class in the constructor. That is what I demonstrate in this partially complete code below.

function requireAuth(nextState, replace, callback) {
  const { store } = this.props;
  const { getState, dispatch } = store;

  ...
}

export default class MyRouter extends React.Component {

  constructor() {
    super();

    this.requireAuth = requireAuth.bind(this);
    // The Hot Loader Bug I ran into it makes this not work
    // this.requireAuth = this.requireAuth.bind(this);


    // Configure routes here as this solves a problem with hot loading where
    // the routes are recreated each time.
    this.routes = (
      <Route path='/' component={ SomeComponent } >
        <IndexRoute component={ Home } />
        <Route path='about' component={ About } />
        <Route path='login' component={ Login } onSubmit={ this.login } />
        <Route onEnter={ this.requireAuth } component={ SecretStuffComponent } />
        <Route path='*' component={ PageNotFound } />
      </Route>
    );
  }

  // The Hot Loader Bug I ran into it makes this not work
  // requireAuth(nextState, replace, callback) { ... }

  render() {
    const { history } = this.props;
    return (
      <Router history={ history }>
        { this.routes }
      </Router>
    );
  }
}

Hope that answers your question?

@dpwrussell

I saw your post at 329. I had the same issue with hook.length always 0.

I got it to work by wrapping my requireAuth in a function:

<Route path="/" component={SecretStuffComponent} 
 onEnter={(nextState, replace, callback) => { this.requireAuth(nextState, replace, callback) }}>

That seems to keep the hook.length at 3.
My hot-loader build seems to work.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

maier-stefan picture maier-stefan  路  3Comments

sarbbottam picture sarbbottam  路  3Comments

Waquo picture Waquo  路  3Comments

ArthurRougier picture ArthurRougier  路  3Comments

yormi picture yormi  路  3Comments