When talking about interactive single-page web applications (SPA), the issue of URL-based routing takes a particular form. There are no “pages” to navigate to since technically only one document exists. In other words, the resource being located is the app itself.

Resource identification

So we can say that in an SPA there is only one resource and many sections that need to be hidden or shown depending on the state. These sections might actually look a lot like pages in a multi-document site but because of the architecture chosen they do not enjoy the same inherent benefits. Namely, the ability to affect the browser’s history and to be bookmarked or linked externally.

Luckily the standard has always accounted for this possibility and provided us with a so-called fragment identifier or hash. If the URI represents our entire application, the fragment can extend that reference to identify any number of subordinate resources which in our case amount to every possible object.

The URL as an interface

It is difficult then not to think of the URL as part of the application state given the prominency and persistency of the address bar. One could even attempt to encode the entire state into the hash. While possible, such implementation could prove problematic for anything other than tiny apps, since even the data being fetched would also need to be assimilated into the state and consequently the URL.

The address bar can also be thought of as a sort of secondary UI. The user interfaces with it by adding or modifying parts of the line that comprises the fragment. Consider the following example of a calculator app:

https://calculator/add#a=4&b=8

The user inputs the values and in response they get a freshly evaluated fragment that includes the result of the calculation:

https://calculator/add#a=4&b=8&result=12

It is important for the developer to be aware of the dual-command situation where both the DOM and the address bar function as input/output channels. The key for taking full control of this behaviour is to always keep the URL consistent with what is displayed on screen as the state variables in the javascript runtime.

Application state

Adopting an unidirectional data-flow or Flux architecture can help us solve the problem of keeping the URL and the DOM synchronized with the state. With a framework like Redux, everything that is performed on the screen or views is compiled into an action containing a pre-processed payload. The action is dispatched and ultimately absorbed (reduced) into the current application state representation.

Once the new version of the state is assembled, the views get notified so they can update themselves and communicate to the user the results of their latest command. The views typically consist of rectangular areas of the window including - why not - the browser’s address bar.

Controlling the address bar

Javascript provides us with an excellent method for managing the fragment on the address bar. Subscribing to the onHashChange event will give us a chance to create and dispatch an action every time the URL is manipulated. This includes indirect manipulation like following a hyperlink.

Furthermore, when wishing to notify the user via address bar, the window object has us covered with the well-known location property. To complete the puzzle we only need the ability to encode partial state into the hash and then decode it in order to put together the action.

In order to solve the last piece of our routing puzzle we need powerful pattern-matching tools. Luckily, regular expressions support is built into every version of javascript. We can utilize this to parse complex expressions like URI-like text encoded in the fragment. That gives us all the elements we need to solve our “routing” problem without recurring to ad-hoc libraries.

Examples

Consider a site with a home and an about page. When the about page is being displayed on screen, we want the hash to reflect this:

#page=about

Assembling such URL using a query string format is fairly straight forward:

  if (store.getState().page === 'home') {
    window.location.hash = ''
  } else {
    window.location.hash = `#page=${store.getState().page}`
  }

To decode this query-string format we would need to use a parser or simply build our own:

const parts = hash.split(/[#&]/)
if (empty(parts) || noPageSpecified(parts)) {
  store.dispatch(actions.goHome())
}
for (const p of parts) {
  const pageMatches = p.match(/^page=(about)$/)
  if (pageMatches !== null)
    store.dispatch(actions.goToPage(pageMatches[1]))
}

There’s a clear advantage to this approach which is that it lets the developer determine what the most appropriate format is.

For a complete working example, checkout this project.

Conclusion

While it is tempting to search the web for drop-in solutions to a common problem like routing, sometimes these libraries do not integrate well into our code base and may even impose extra restrictions on the architecture. Part of adopting a framework like Redux means that we have to trust it with the issues that are present in almost every web development project. We might find that -as in this case- the original issue is reduced to just another instance of input coming in and output coming out. Something we know exactly how to handle with our existing toolkit.