Async authentication with React-Router 1.0.0-beta2 and Flux
The latest 1.0.0-beta2
release of react-router has changed a lot from earlier versions, particularly in the way you can hook into a transition. In an earlier post I looked at a way of implementing react-router auth with a Facebook Flux architecture. This was using react-router 0.13.3
. What follows is a substantially different approach to achieve the same kind of result, utilizing the new 1.0.0-beta
features.
tl;dr
React-Router 1.0.0-beta2
now requires transition hooks to be defined on the Route
in the root level Router
JSX tree (if you are using JSX). willTransitionTo
(defined on the component) has been replaced by onEnter
(defined directly on the <Route>
). This significantly changes how you can ‘interrupt’ a transition to do some asynchronous authentication, before continuing to an authenticated page.
A new hope
Despite the fact that a lot has changed in react-router 1.0.0-beta2
and that it has incomplete documentation and no current changelog it offers some good things as can be evidenced by looking at the recently updated examples, and what documentation does exist. For example, it is now possible to use ReactCssTransitionGroup
to animate transitions between routes.
Some new problems
In the last few days of using the new react-router, I have struggled to intercept and asynchronously resume the first transition. This is partly because the willTransitionTo
hook (and the parameters that it exposed) is no longer available. This hook previously exposed a callback
which you could invoke after some promise-returning async functionality. This does not seem to be possible in the new onEnter
hook which is defined directly on the <Route>
.
Another thing I noticed is that if you try and transition.abort()
the first transition, then you get the following warning:
Uncaught Error: Invariant Violation: You may not abort the initial transition
My async use case
Each time I start/reload my app, I initiate an auto-login sequence whereby I check for a locally stored token. If a token exists, I then send it to the server to check it is still valid, and if so, retrieve some user data. I’m using JWT tokens which can expire and only contain a hashed key. Only after I get a response to the token can I tell if I am logged in or not.
I also want to be able to deep-link into any authenticated page in my app as the first route. So I need to be able to intercept this first transition and wait for an async response from the server.
A slightly hacky solution
var Shim = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
transitionCheck(){
var router = this.context.router;
//if not waiting for some async login outcome then re-direct to login
if (!auth.isAsyncLoggingIn()){
router.transitionTo('/login');
}
//if you've gone here by accident
if(auth.isLoggedIn()){
router.transitionTo(nextPathStore);
}
},
componentWillMount() {
this.transitionCheck();
},
render() {
//can leave this blank
return <div>waiting...</div>;
}
});
I’m basically defining a ‘shim’ route. This is a go-between that gets around the problem of aborting the first transition. An authenticated route will redirect to it if the client is not logged in. Once I get to the shim page, I stay there and allow the component instance lifecycle to proceed and the first transition to effectively end.
I can use the componentWillMount
hook in the shim, or even the constructor (if I’m using ES6 classes), to access this.context.router
to start any new transitions. NB I cannot access this.context.router
in the static onEnter
hook of a page component before it has instantiated.
In most cases I will only be re-directed to this shim page when auto-logging in. In which case I can wait for the login sequence to be resolved either way and then the root App
component will listen for any login changes and re-direct accordingly.
//auth login/logout module listener
authListener(loggedIn) {
this.setState({
loggedIn: loggedIn
});
//re-direct post login or logout
if(loggedIn){
this.context.router.transitionTo(nextPathStore);
}else{
this.context.router.transitionTo('/login');
}
}
However if I re-direct there when there is no token available for auto-login, then there will be no ongoing login sequence. So the shim must detect if any async logging-in is occurring. If not then it immediately re-directs to the login page.
It is also just possible that through some weird clicks a user could end up in the shim even though they are logged in. In which case I just re-direct to the last stored authenticated route path.
I’ve put an implementation of this async auth process in a forked version of the react-router examples here:
https://github.com/wmyers/react-router/blob/master/examples/auth-flow-async/app.js