Web Application Routing
Since the rise of single-page applications (SPA), web applications have become ever more complex. Many web frameworks, such as React, Angular, Elm, Backbone, etc., have been created to help tame the complexity of web apps. One of the most important strategy for tackling complexity is through divide and conquer, or equivalently, through composition — by dividing the problem into small enough, manageable chunks, solving them, and then composing them together to form the full solution. Therefore, to tame the complexity of web apps, we need a mechanism to divvy up the problem. Routing, or component plus router, as we will make clear in this article, is precisely such a mechanism.
The UI Tree
Consider web app UIs built from reusable web components through composition. Each such UI then corresponds to a tree of components. We emphasize the tree we are considering is not a snapshot of the UI view, but the collection of all possible UI views. Thus, for example, different selected tab corresponds to different branch in the tree, etc. Each node in this tree consists of a web component. Each non-leaf node is a web component that can contain nested child web components. Any node, together with all its ancestor nodes, yields a snapshot of the UI view (or, more precisely, an equivalence class of UI view snapshots).
With the above UI tree model it is now easy to explain what routing is. Routing is a framework that maps the UI tree nodes to the URL paths and vice versa. Each branch of the tree is assigned a path. The route (URL path) to a node is obtained by joining all paths from root to the node by /. This assignment drives UI navigation in the sense that the UI view for a given URL path consists of the UI view snapshot mentioned above by composing all web components on the path. I.e., for that URL path, only the web components on the path are in the DOM and contribute to the UI rendering.
A General Example
To make the discussion more concrete, consider a web app UI with m-level-deep tabs. (For simplicity, we only consider tabs here. However, the approach is generally applicable to any UI partitioning/branching constructs such as drop-downs, menus, selectable master table, etc.) To specify the routing, we will use Angular router as an example. In this case the routes specification might look like the following:
The naming convention is (_level_index)*. So C1_1_2_3 corresponds to the component where the first tab in the first level and the 3rd tab in the second level are active. According to the routes above, the URL path / will be redirected to /t1_1, which in turn will be redirected to its default children at /t1_1/t2_1 etc. Thus the UI at / will be served by the composition of components C1_1 and C1_1_2_1 etc.
While the above approach conveniently keeps all routes in a single place, for a large app it can quickly get out of hand and become very difficult to maintain. In this case what we need is again composition. We could divide the problem so that each node has its own routes file which aggregates routes exported by its children nodes. This divides the problem into one-level-deep sub problems. Taking this approach the top-level routes file becomes:
While the next level routes files might look like:
And the last level routes files might look like:
To support nested routes, the routing framework must provide a way for the parent component to specify where the children components should be inserted. For Angular router, this is the <router-outlet/> tag. For ui-router, it is the <ui-view/> tag (or <UIView/> for React ui-router).
A routing framework also typically provides easy ways to link to and navigate to routes. For example, the routerLink and routerLinkActive directives in Angular router:
Thus, clicking on Tab1_i will navigate to the route t1_i and add the CSS class active to the tab.
Some route path element may take variable values. Typically this is supported via the notation ‘:variable-name’ for the path element value.
Additionally, more advanced router may support parallel routes. In Angular router, for example, one can assign different names to different parallel router-outlets. In this case the routes mapping can also take an outlet parameter. Parallel routes are a perfect tool to support popups and dialogs routing.
Value of Routing
One can build the same web application UI tree without routing, so what are the advantages with routing? To name a few:
- Reduce accidental complexity, provide a pattern to solve a common problem.
- Give more structure to web application design and implementation
- Make web applications easier to understand, debug, and extend
- Simplify UI navigation, support deep linking
- Enforce separation of concern, make it easy to change route wiring
So, if you haven’t done so already, for your next project: try to maximize the use of your routing framework (by making your entire UI routable), avoid any ad hoc navigation and view switching. You may be pleasantly surprised with the outcome!