Client-side routing and parameters

Posted by Bertalan Miklos on 2017-01-11

Introduction

This blog post was inspired by this article, which explains the author’s issues with SPAs. His main point is that a lot of single page apps don’t behave like proper web pages.

A single-page application (SPA) is a web application or web site that fits on a single web page with the goal of providing a user experience similar to that of a desktop application.

According to Wikipedia (and me) a SPA is still web page, which means it should be bookmarkable, refreshable, navigatable with history buttons and shareable by links. Using an app that doesn’t do these can be very frustrating and writing an app that handles them all can be very challenging.

A SPA should be bookmarkable, refreshable, navigatable with history buttons and shareable by links.

The aforementioned issues are all routing related and they are all general enough to be handled at a framework level. There are many client-side routing libraries in the wild with slightly different flavors, but most commonly they are inspired by server-side routers.

In this article I explain my view on client-side routing and the main idea behind the routing related NX middlewares. Finally I provide some examples - written in NX - to prove my theory. The first few sections are framework independent, while the end is NX specific.

Client-side vs server-side routing

A typical server-side routing snippet looks like this.

app.get('/users/:userId/books/:bookId', (req, res) => {
res.send(req.params)
})

It routes the request to the handler, which sends a response based on the parameters. The handler runs once for each request and thats it.

The client-side has pages instead of handlers. Unlike handlers, pages tend to stick around for a while and parameters can frequently change during this. Server-side inspired routers usually handle static paths well, but they have problems with dynamic parameters.

Handling parameters

A page’s parameters must be reflected in the URL and browser history and vice versa. A commonly used hack is to re-route the application to the same page with the new parameters whenever a parameter changes.

state.param = 'newValue'
router.go('currentPage', {param: 'newValue'})

This is very similar to the server-side solution, but running the route handler on parameter changes raises two issues.

  • Depending on the implementation of router.go, the page might be totally re-initialized and re-rendered.
  • Calling router.go every time a state property changes is time consuming and easy to forget. A declarative solution would be nicer.

The solution is separation

NX separates routing into static path routing and dynamic parameter routing.

The path serves as a static unique identifier for a page and it works like server-side routing. Changing it runs a handler once, which displays the correct page. The dynamic parameters must be stored in the query strings and they are automatically reflected in the current page’s state. The parameter handler is client-side specific and it may run multiple times while the page is active.

As an example the previously used /user/12/books/5 would turn into /user/books?userId=12&bookId=5. It looks a lot uglier, but it comes with two big advantages.

  • It allows optional parameters, as they are never hard coded as path tokens.
  • It makes it possible to decouple path routing from parameter routing.

Path routing

Path routing is really simple without dynamic parameters. In fact it is simple enough to be configurable by a few HTML attributes only. The below example is a fully functional client-side routed NX app with two pages and a navigation bar.

nx.components.app().register('routing-app')
nx.components.router().register('router-comp')
<routing-app>
<a iref="home">Home</a>
<a iref="profile">Profile</a>
<router-comp>
<h2 route="home" default-route>Home page</h2>
<h2 route="profile">Profile page</h2>
</router-comp>
</routing-app>

Path routing

The router-comp component simply changes it’s content based on the current URL path.

  • If the current url is page.com/profile, the profile page is displayed.
  • If the current url is page.com/home, the home page is displayed.
  • In every other case the home page is displayed as it is tagged as the default-route.

Navigation is equally simple. Anchor elements change the path to match with their iref attribute on click. Clicking the anchor with iref="home" changes the URL path to page.com/home and updates the router to display the home page.

If you would like to learn more about path routing in NX, check the related docs page. It has editable examples for nested routers, navigation options and router events.

Parameter routing

Parameter routing reflects the state of the current page in the query string and the browser history and vice versa. Parameters can be declaratively configured for every page. A simple and fully functional example looks like this.

nx.components.app()
.use(nx.middlewares.params({
name: {history: true, default: 'World'},
}))
.register('greeting-app')
<greeting-app>
<p>Name: <input name="name" bind></p>
<p>Hello @{name}!</p>
</greeting-app>

Parameter routing

The greeting-app automatically synchronizes its state’s name property with the name query parameter whenever either of these change. If you would like to know more about parameter routing in NX, check the related docs page.

Reunion

Path routing creates the skeleton of the page, while parameter routing handles the parameters of the current page. They work independently, but they can be used together as a full routing solution.

nx.components.app().register('routing-app')
nx.components.router().register('router-comp')
nx.component()
.use(nx.middlewares.params({
name: {history: true, default: 'World'},
}))
.register('greeting-comp')
<routing-app>
<a iref="home">Home</a>
<a iref="greeting">Greeting</a>
<router-comp>
<h2 route="home" default-route>Home page</h2>
<greeting-comp route="greeting">
<p>Name: <input name="name" bind></p>
<p>Hello @{name}!</p>
</greeting-comp>
</router-comp>
</routing-app>

Combined routing

This example has two pages and it switches between them when the path or the browsers history changes. The home page doesn’t have any state, while the greeting page has a state with a name property. It is kept in sync with the URL query and the history by the parameter routing. Changing the name parameter re-renders the Hello @{name}! text node only.

Conclusion

I hope you found this a good read. If you are interested in a complete guide for the example app, check the Getting started page. If you have any thoughts on the topic, please share them in the comments.

Have a nice day!


Comments: