Using UI-Router for better React routing

Scott Carmichael
8 min readDec 2, 2017

--

(I have since written a revision for this post for a newer version of UI-Router. The concepts are the same; just updated to reflect some minor changes in how UI-Router should be implemented. This can be found here.)

React has a key advantage over other frameworks. It’s very selective about what ships with the core. This means developers are able to find and install packages that best suit their project. Resulting in a leaner framework and making it very extendable.

Routing is a major element when building single page web apps. React doesn't have the capability to it out of the box. No built in routing library ships with the framework, you have to install your own. This is one area React gives developers plenty of rope to hang themselves with.

The go to solution for adding routing to a React app is the terrible React Router package. I know I’m not alone on this. I have regular discussions with other devs about how frustrating it is to use in a project.

Implementing routes as components is a lot of hassle and is not very pretty to look at. If you have an app with many routes; this is a real pain to manage. The app I work on daily has over 100 different routes. React Router would just not be a practical solution if we ever decide to rebuild.

The alternative I would go for is UI-Router. This router is framework independent, this is intentional. I’ve had a fair amount of exposure and success with UI-Router from my time working on AngularJS projects. It does have a dedicated React package, so adding it to a React project is a fairly easy process. This is how I use it in my React projects.

Checkout this demo I put up on Github for a working example.

Router Setup

The UIView component allows route content to be loaded into an area of the app’s wrapper component. This allows for common elements like the app’s header and footer to display on every route.

// App.jsx
import React, { Component } from 'react';
import { UIRouter, UIView } from '@uirouter/react';
import { router } from './router';
class App extends Component { render() {
return (
<UIRouter router={router}>
<div>
<UIView/>
</div>
</UIRouter>
);
}
}export default App;

The router setup file is where the router instance is configured and started. The router object needs to be passed to the top level UIRouter (shown above).

// router.jsx
import { UIRouterReact, servicesPlugin, hashLocationPlugin } from '@uirouter/react';
// Create router instance + setup
export const router = new UIRouterReact();
router.plugin(servicesPlugin);
router.plugin(hashLocationPlugin);
// Start the router
router.start();

Exporting the router object is important to make it accessible to the rest of the files in the app. Many of the router services are found on this object.

App Structure

React is modular by nature and is a good approach for structuring an app into routes. Smaller, separate modules help to group and organise associated files. Modules typically include a component file along with other asset files (CSS, tests etc.). The purpose of the router is to stitch them all together.

Modules make the codebase easier to navigate. This has a real benefit for larger apps which are likely to have many routes. Ensuring things are easy (and obvious) to find.

UI-Router is state based and navigation is handled by the state machine. This differs from React-Router which is driven by matching browser urls. UI-Router does allow you to build apps without declaring urls. Although, this is confusing and prevent you from implementing url deep linking.

Every route in the app can be described as a state. UI-Router slots into a modular app structure. I include a state definition file in each module directory to make it easy to find in the project.

State Definition and Registration

A state definition file outlines key properties and behaviours about a state. The state name, url and component (to load) are declared here. A simple state definition file looks like this.

import ContactsPage from ‘./contacts’;export default {
name: 'contacts',
url: '/contacts',
component: ContactsPage,
}

For states to take affect in the app, they must first be registered using the state registry service. The router setup file is an ideal place to put this.

// router.jsx
import { UIRouterReact, servicesPlugin, hashLocationPlugin } from '@uirouter/react';
// Import states
import contact from './modules/contact/state';
import contacts from './modules/contacts/state';
// Create router instance + setup
export const router = new UIRouterReact();
router.plugin(servicesPlugin);
router.plugin(hashLocationPlugin);
// Register each state
const states = [
contact,
contacts
];
states.forEach(state => router.stateRegistry.register(state));
// Start the router
router.start();

I like this approach as it makes the codebase easy to navigate. Higher level setup is centralised in the project. State (specific) logic remains part of individual modules.

State Params

State params can be declared as part of the url value. This allows for params to be persisted in the url if the page is reloaded or shared.

State params are indicated in the url by prefixing them with a colon or wrapping them in curly braces. The example below uses the colon syntax to declare a contactId param.

export default {
name: 'contact',
url: '/contacts/:contactId'
}

The query string can also be used to set multiple, less crucial params. This approach has the advantage of making params optional. Meaning states can be loaded without having to provide every declared param. This is useful for handling things like pagination and can help keep the url human readable.

export default {
name: 'contacts',
url: '/contacts?{page}&{perPage}'
}

Params can be accessed by the state components as key/value pairs from the globals object on the router. The keys will map the params declared in the url value.

Transitions

To trigger a transition; state names are used, not urls. The easiest way to change state is using the UISref component, which is a glorified anchor tag. This wraps an html element (or text node) and turns it into a UI-Router link.

<UISref to={‘contact’} params={{contactId: 1}}> 
<a>Joe Bloggs</a>
</UISref>

This can be handled programatically too using the state service. The go method achieves the same thing only as a function call instead. It takes two parameters; a name and a params object. This is useful for triggering transitions from a callback or another function.

router.stateService.go('contact', {contactId: 1});

Information about the current state can be accessed from the state service too. This includes general info like the state’s name. When updating state params with no transition. I like to set the name parameter dynamically when using the go method. This makes a component more flexible if it happens to be mapped to multiple states.

const stateName = router.stateService.$current.name;
router.stateService.go(stateName, {page: 2});

Transition Hooks

UI-Router makes it pretty easy to hook into the different points of a transition. The transition service has a number of methods to achieve this. The three I commonly use are shown below.

router.transitionService.onBefore(true, function(trans) {
// Start transition
});
router.transitionService.onSuccess(true, function(trans) {
// End transition
});
router.transitionService.onError(true, function(err) {
// Transition errored
});

These methods are quite powerful. They allow for callbacks to be run at specific points during a transition. A reference to the transition is also passed to the callback, meaning it can be intercepted if needed. This is useful for handling authentication and locking down states.

I like to show a loading indicator between transitions starting and ending. This gives users some visual feedback that a page is loading. As they will fire during every transition, the before and success hooks are perfect for this. Be sure to tidy up actions in the error hook too. If the transition fails the error should be caught, handled and indicated to the user.

Resolves

Resolves are a great way of fetching data before transitioning to a state. Key data can be loaded and made available before the state component is mounted. The example state below resolves some static JSON data.

import ContactsPage from ‘./contacts’;export default {
name: 'contacts',
url: '/contacts',
component: ContactsPage,
resolves: [
{
token: 'contacts,
resolveFn: () => {
return [
{
name: 'Joe Bloggs',
age: 21,
country: 'Scotland'
},
{
name: 'Jane Doe',
age: 31,
country: 'England'
},
{
name: 'John Smith',
age: 41,
country: 'Wales'
}
];
}
}
]
}

The resolves property is an array that consists of individual resolve objects. The token property is the resolve’s unique key. The resolveFn property must be a function that returns the data.

The above example uses static data and will be available immediately. The resolveFn function also supports returning promises. This is a powerful tool for handling deferred http data requests to an API. UI-Router will wait until the promise(s) has resolved before completing the transition.

If the promise fails the transition will also fail and the error hook will fire. Be sure to set up an error hook (see above) to give the user feedback about why the transition failed.

However, over using resolves like this will slow down the time to complete a transition. Choose the key data needed before the state component is mounted. Lazy load lesser data afterwards.

Below is an example using a promise based data service and a param declared in the url.

import ContactPage from './contact';
import contactsService from './contacts-service'
export default {
name: 'contact',
url: '/contacts/:contactId',
component: ContactPage,
resolve: [
{
token: 'contact',
deps: ['$transition$'],
resolveFn: (trans) => {
// Get contactId param
const contactId = trans.params().contactId;
// Fetch data
return new contactsService.get(contactId);
}
}
]
};

The transition must be included as a dependency (deps) to access any state params in the resolveFn. This is then available as parameter to the function. Resolved data can be accessed in the state component via it’s props (see below).

import React, { Component } from 'react';export class ContactPage extends Component {    // Get contact resolve via the component's props
contact = this.props.resolves.contact;
render() {
return (
<h1>
{this.contact.name}
</h1>
);
}
}export default ContactPage;

Be wary of state params or resolved data changing after the component has been mounted. The componentWillReceiveProps method is sometimes needed to handle changed state params. This typically happens when params are set on state.

I like this approach as it moves data loading logic out of the component. This makes state components more testable by separating it from data services. Allowing for data to be easily mocked via component props.

In Summary

UI-Router has a dedicated React package so getting it up and running is fairly easy. Beyond that, I’ve found it to be the ideal router for building web apps with React.

  • It works extremely well with a modularised app structure.
  • Higher level routing config is handled separately from individual states.
  • Route params can be declared, allowing for dynamic pages and url deep linking.
  • Custom callbacks can be fired at each stage of a transition using transition hooks.
  • Resolves allow for key data to be loaded before the state component is mounted.

Checkout the working demo I have up on Github. If you’ve been frustrated by React Router or with routing in general, give UI-Router a try. It’s a powerful bit of kit and is very accessible to newbies.

(If you’re looking for a good overview about the differences between React-Router and UI-Router, this post by Marco Botto is well worth a read. — http://marcobotto.com/overview-of-ui-router-react-and-comparison-with-react-router/)

--

--