The JavaScript ecosystem, for better or worse, is in a constant state of change and disarray. From the NodeJS fork to io.js and later reconciliation to the npm package-gate which broke many packages and ruined a lot of peoples day. The constant in all of this turbulence is that the JavaScript community was quick to react and resolve the issue for the better.

The latest discord comes from the popular and heavily-depended upon React Router library, which provides a routing framework for applications built with React. React Router is a community project with no direct affiliation to Facebook or React but is a major dependency for many developers building React apps.

React Router was forked into rrtr by Jimmy Jia, a longtime contributor to the project, last week after complaints that React Router has fallen into a slow release cycle, is missing critical features and more. A few days later, the rrtr library was itself deprecated and users told to switch back to React Router. Jimmy was made an owner of the React Router project so that he could further his contributions to the project.

React Router Alternatives

React Router is the de-facto routing library for React. In our brief post today, we'll take a look at some React Router alternatives.

"React Router is the de-facto routing library for React apps."

React Router Component

React Router Component is a declarative router component for React. Routes in this library are declared directly as part of your component hierarchy. Having routes defined as a part of your component hierarchy allows for dynamically reconfiguring routing based on application state. An example of the React Router Component in action:

var App = React.createClass({
  render: function() {
    return (
      <Locations>
        <Location path="/" handler={MainPage} />
        /* Check if user is logged in, redirect to login page if not */
        <Location path="/account/:username" logged_in={this.state.logged_in} handler={this.state.logged_in ? AccountPage : createRedirect("/login")} />
        <Location path={/\/friends\/(\d+)\/(photos|wall)/} logged_in={this.state.logged_in} handler={FriendsPage}
      matchKeys={['id', 'pageName']} />
      </Locations>
    )
  }
})

React Mini Router

The React Mini Router is a minimal routing library for React apps. It has few external dependencies and comes in at a tiny 4kb when gzipped. This routing library works by declaring the routes at the root level of the React app. This may be a good alternative for simple React apps. The React Mini Router library does not have pre or post hooks for routes so any logic for checking if a user is authenticated should be handled within the route itself.

var React = require('react'),
  RouterMixin = require('react-mini-router').RouterMixin;

var App = React.createClass({

  mixins: [RouterMixin],

  routes: {
    '/': 'home',
  },

  render: function() {
    return this.renderCurrentRoute();
  },

  home: function() {
    return <div>Hello World</div>;
  },

  notFound: function(path) {
    return <div class="not-found">Page Not Found: {path}</div>;
  }

});

module.exports = App;

Universal Router

Universal Router provides a simple routing solution for JavaScript built apps including React. The benefits of universal router are that it uses the same middleware approach as Express which makes it very easy to pick up, learn and extend. An example of universal router in action:

const authorize = (state, next) => {
  // Check if user is logged in
  if (!state.isAuthenticated) {
    state.redirect = '/login'; next();
  }
}
const router = new Router(on => {
  on('*', async (state, next) => {
    const component = await next();
    return component && <App context={state.context}>{component}</App>;
  });

  on('/admin', async (state, next) => {
    // Ensure user is logged in
    authorize(state, next);
    return (
      <AdminPage />
    )
  });
})

router5

router5 is a framework agnostic routing solution that is not limited to only React. It treats routing like any other data or state and handles both route and data updates. router5 was designed for component trees which makes it a great fit for React based applications. Here is an example of router5 with React in action:

import Router5, { loggerPlugin } from 'router5';
const router = new Router5()
  .setOption('useHash', true)
  // .setOption('hashPrefix', '!')
  .setOption('defaultRoute', 'home')
  // Routes
  .addNode('home', '/home')
  .addNode('account', '/account', canActivate : isLoggedIn)
  .addNode('messages', '/messages', canActivate: isLoggedIn)
  // Plugins
  .usePlugin(loggerPlugin())

export default router;

For additional resources on router5 check out their Github repo and the helper library for React.

Build Your Own React Router

If you are feeling adventurous and up for a challenge, James K Nelson has written a great tutorial on building your own routing solution with React. His tutorial covers a lot and is a great starting point for learning and understanding how state based routing works.

Aside: Authenticate a React App with Auth0

We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with a React application using Auth0.

Auth0 login screen

We'll need an Auth0 account to manage authentication. To sign up for a free account, we can follow this link. Next, let's set up an Auth0 client app and API so Auth0 can interface with a React App.

Setting Up a Client App

  1. Let's go to our Auth0 Dashboard and click the "create a new client" button.
  2. Let's call our app as "React Demo" and select "Single Page Web Applications".
  3. In the Settings for our new Auth0 client app, let's add http://localhost:3000/callback to the Allowed Callback URLs.
  4. If desired, we can set up some social connections. We can then enable them for our app in the Client options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter. For production, make sure to set up the correct social keys and do not leave social connections set to use Auth0 dev keys.

Set Up an API

  1. Go to APIs in your Auth0 dashboard and click on the "Create API" button. Enter a name for the API. Set the Identifier to your API endpoint URL. In this example, this is http://localhost:3001/api/. The Signing Algorithm should be RS256.
  2. You can consult the Node.js example under the Quick Start tab in your new API's settings. We'll implement our Node API in this fashion, using Express, express-jwt, and jwks-rsa.

We're now ready to implement Auth0 authentication on both our React client and Node backend API.

Dependencies and Setup

There are only two dependencies that we really need to install: auth0.js and history. To do that, let's issue npm install --save auth0-js history in the project root.

Note: As we want the best security available, we are going to rely on the Auth0 login page. This method consists of redirecting users to a login page hosted by Auth0 that is easily customizable right from the Dashboard.

After installing it, we can create an authentication service to interface with the auth0.js script. Let's call this service Auth and create it in the src/Auth/ directory with the following code:

import history from '../history';
import auth0 from 'auth0-js';

export default class Auth {
  auth0 = new auth0.WebAuth({
    // the following three lines MUST be updated
    domain: 'bkrebs.auth0.com',
    audience: 'https://bkrebs.auth0.com/userinfo',
    clientID: '3co4Cdt3h3x8En7Cj0s7Zg5FxhKOjeeK',
    redirectUri: 'http://localhost:3000/callback',
    responseType: 'token',
    scope: 'openid'
  });

  constructor() {
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.isAuthenticated = this.isAuthenticated.bind(this);
  }

  handleAuthentication() {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken) {
        this.setSession(authResult);
        history.replace('/home');
      } else if (err) {
        history.replace('/home');
        console.log(err);
      }
    });
  }

  setSession(authResult) {
    // Set the time that the access token will expire at
    let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('expires_at', expiresAt);
    // navigate to the home route
    history.replace('/home');
  }

  login() {
    this.auth0.authorize();
  }

  logout() {
    // Clear access token and expiration from local storage
    localStorage.removeItem('access_token');
    localStorage.removeItem('expires_at');
    // navigate to the home route
    history.replace('/home');
  }

  isAuthenticated() {
    // Check whether the current time is past the
    // access token's expiry time
    let expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return new Date().getTime() < expiresAt;
  }
}

The Auth service just created contains functions to deal with various steps of the sign in/sign up process. The following list briefly summarizes these functions and their descriptions:

  • handleAuthentication: looks for the result of the authentication in the URL hash. Then, process the result with the parseHash method from auth0-js;
  • setSession: sets the user's access token and the access token's expiry time;
  • login: initiates the login process, redirecting users to the login page;
  • logout: removes the user's tokens and expiry time from browser storage;
  • isAuthenticated: checks whether the expiry time for the user's access token has passed;

Besides these functions, the class contains a field called auth0 that is initialized with values extracted from the Auth0 client. Let's keep in mind that we need to update them accordingly before proceeding.

Attentive readers probably noticed that the Auth service also imports a module called history that we haven't talked about. We can define this module in only two lines, but let's define it in a file to provide reusability. Let's call this file ./src/history/history.js and add the following code:

import createHistory from 'history/createBrowserHistory'

export default createHistory()

After creating both elements, we can refactor our App component to make use of the Auth service.

import React, { Component } from 'react';
import { Navbar, Button } from 'react-bootstrap';
import './App.css';

class App extends Component {
  goTo(route) {
    this.props.history.replace(`/${route}`)
  }

  login() {
    this.props.auth.login();
  }

  logout() {
    this.props.auth.logout();
  }

  render() {
    const { isAuthenticated } = this.props.auth;

    // ... render the view
  }
}

export default App;

Note that we are passing this service through props. Therefore, when including the App component, we need to inject Auth into it: <App auth={auth} />.

Considering that we are using the Auth0 login page, our users are taken away from the application. However, after they authenticate, users automatically return to the callback URL that we set up previously (http://localhost:3000/callback). This means that we need to create a component responsible for this URL:

import React, { Component } from 'react';
import loading from './loading.svg';

class Callback extends Component {
  render() {
    const style = //...

    return (
      <div style={style}>
        <img src={loading} alt="loading"/>
      </div>
    );
  }
}

export default Callback;

This component can just contain a loading indicator that keeps spinning while the application sets up a client-side session for the users. After the session is set up, we can redirect users to another route.

Please refer to the official Quick Start Guide to see, step by step, how to properly secure a React application. Besides the steps shown in this section, the guide also shows:

Conclusion

The JavaScript community is constantly changing. Frameworks, libraries and conflicts come and go. React Router is and will likely remain the go-to routing library for React but that's not to say that there aren't great alternatives worth checking out. The co-maintainers of the React Router library have pledged to take better steps in terms of communication, release schedule and merging of pull requests for the React Router library and I'm excited to see those changes implemented.