Authentication routing in a React-Flux application

Using react-router it is not immediately clear how to integrate routing into a React-Flux app.

tl;dr

My proposed solution is to only reference the react-router directly in React components, i.e. not reference the router in Flux actions, dispatchers and stores - so there is only a dependency between the router and the React component layer. I will also define a RouterStore that stores the next router transitionPath string when a user attempts to go to a page which requires authentication and they are not logged in.

You can check out the source code here.

You will also need to clone a modified version of Auth0’s NodeJS JWT API here.

Preamble

Coming from AngularJS, you can get used to a lot of app infrastructure being provided for you. Angular is still a complicated and powerful beast. But React-Flux involves throwing out the MVC rulebook at the start, and implementing your own core functionality for things like http requests.

Importantly React is just the view layer, albeit one that does a lot of clever stuff. For the rest of your app the Flux architecture is recommended to work well with React. Using bundlers like Browserify and Webpack, you can implement NodeJS and NPM modules on the client side to create your Flux application structure. I’m using Facebook’s Flux implementation which does provide a singleton Dispatcher module.

This post assumes an understanding of React, Flux, Browserify etc. For a good introduction to React and Flux see the Scotch.io tutorials.

ES6 syntax

Because React-Flux is quite new, its rise in popularity is coinciding with the take up of Javascript ES6 syntax (and in some cases ES7, which is stage 2 ES6). Using Babel (previously 6to5) you can code in ES6 and transpile to ES5. For Browserify bundles, there is the Babelify transform module. Much of the code in this post is ES6 syntax.

React-Router

Comparing with Angular again, like many others I’ve used the well-known (and well-tested) ui-router. It can be defined and used in different ways in nested Angular modules, and works with the Angular application framework as a whole.

react-router is a bit different, in that it is built to work with React component views, but not really for any encompassing architecture like Flux. So react-router and Flux are a bit of an odd couple. Nevertheless it is quite possible to get them working together.

There are a few proposed techniques already out there, but not many for authentication, which can be more fiddly. The following approach draws on the experience and copies techniques from others, particularly:

Src directory structure

js/
---- actions/
-------- LoginActionCreators.js
-------- RouterActionCreators.js
---- components/
-------- App.jsx
-------- AuthenticatedComponent.jsx
-------- Home.jsx
-------- Login.jsx
-------- Private.jsx
-------- Signup.jsx
---- constants/
-------- AppConstants.js
-------- ActionTypes.js
---- dispatchers/
-------- AppDispatcher.js
---- services/
-------- AuthService.js
---- stores/
-------- BaseStore.js
-------- LoginStore.js
-------- RouterStore.js
---- index.jsx
---- router.js
---- routes.jsx
styles/
---- main.css
index.html

Points to note:

  • the router definition, routes definition and router instantiation are in separate files, as per gaeron’s example.
  • the router is defined (in router.js) using the ‘proxy’ approach described here

Routes.jsx

This has a DefaultRoute defined. So if going to the root path ‘/’, the router will redirect to this page.

<Route path="/" handler={App}>
  <DefaultRoute handler={Home}/>
  <Route name="login" path="/login" handler={Login}/>
  <Route name="signup" path="/signup" handler={Signup}/>
  <Route name="private" path="/private" handler={Private}/>
</Route>

stores/LoginStore.jsx

This store deals with all authentication actions. It is updated asynchronously when logging in or signing up via the AuthService, which is triggered by a dispatch from LoginActionCreators.

LoginStore attempts to auto-login in its constructor. So when the user refreshes the app in the browser, any locally stored token will be retrieved. NB in this demo I am not checking if a locally stored JWT token has expired, only if it exists.

_autoLogin () {
  let jwt = localStorage.getItem("my_jwt");
  if (jwt) {
    this._jwt = jwt;
    this._user = jwt_decode(this._jwt);
  }
}

components/App.jsx

In this component I have the only listener function for any LoginStore updates. I only want there to be one listener function for responding to LoginStore updates. In this function a transitionPath value is retrieved from the RouterStore. This value is set whenever a user attempts to go to a page which requires authentication, but they are not logged in. By default RouterStore.nextTransitionPath is set to the root path (‘/’).

_onLoginChange() {
  ...

  //get any nextTransitionPath - NB it can only be got once then it self-nullifies
  let transitionPath = RouterStore.nextTransitionPath || '/';

  if(userLoggedInState.userLoggedIn){
    router.transitionTo(transitionPath);
  }else{
    router.transitionTo('login');
  }
}

The _onLoginChange listener is set up in the componentDidMount hook of the React component lifecycle. This hook only fires when React components are rendered on the client side, and only after child components have mounted.

This component renders a top nav bar with a mutable logged-in/out state. It does not require authentication but it will auto-redirect to its DefaultRoute which does require authentication (read on for more info).

components/AuthenticatedComponent.jsx

This is a higher order component (or Decorator) for making the child component that it wraps, require authentication - ie the user must be logged in before being able to navigate to a page component wrapped in an instance of AuthenticatedComponent.

In this application the Home component page is wrapped in AuthenticatedComponent and Home is the DefaultRoute.

AuthenticatedComponent defines a willTransitionTo hook which is a react-router hook. Whenever this hook gets fired the router is about to transition to the relevant page, so authentication checking can occur here.

static willTransitionTo(transition) {
  if (!LoginStore.isLoggedIn()) {

    let transitionPath = transition.path;

    //store next path in RouterStore for redirecting after authentication
    //as opposed to storing in the router itself with:
    // transition.redirect('/login', {}, {'nextPath' : transition.path});
    RouterActionCreators.storeRouterTransitionPath(transitionPath);

    //go to login page
    transition.redirect('/login');
  }
}

So if a user is not logged in, firstly I’m storing the transition path (e.g. ‘/private’) in the RouterStore. I’m storing it so that it can only be retrieved once, so that old paths don’t persist in the RouterStore. Note that although I could store the next transitionPath in the router itself (using an optional third parameter in a transition.redirect() call), I am choosing not to do so. Instead I am storing it in a Flux store.

Then I am re-directing to the login page.

One other caveat is that a willTransitionTo hook will get called for the first time before a componentDidMount or componentWillMount hook. Something to be aware of.

Conclusion

So the default (not logged-in) use-case works like this:

[Application launches and LoginStore attempts to auto-login, but fails because there is no token] =>

[Application routes to root level path ‘/’] =>

[router redirects to authenticated Home.jsx page] =>

[willTransitionTo hook in Home.jsx stores transition path in RouterStore and then redirects to Login.jsx page] =>

[User logs in and Login.jsx triggers an action in LoginActionHandlers] =>

[LoginStore updates and then emits CHANGE] =>

[_onLoginChange in App.jsx tells router to transition to stored path (or ‘/’)]

Written on June 4, 2015