Routers are a very common and often mature part of the front-end ecosystem. So the question “why build such a wheel?” became a soul question that had to be answered. What drove me to do this in the first place were the following:

  • Common React Router solutions have no centralized routing state, making it difficult to integrate with global state/services outside of components. Sometimes you need to process query parameters multiple times, and the written code feels redundant.
  • There was no type-safe routing implementation at the time, and paths were basically written to strings.

As the project iterated, there was another very good reason:

  • No route implementation supporting parallel routing is seen.

The Boring Router is a React + MobX based Router developed in TypeScript that implements type-safe routing and supports parallel routes that are independent of each other. Currently, the Boring Router has been iterated over for a year and a half and is widely used in our internal projects.

Unlike the common component-first routers in the React ecosystem, the Boring Router focuses on routing state and the component implementation is very lightweight. The basic usage is as follows:

import {RouteMatch, Router} from 'boring-router'; import {BrowserHistory, Link, Route} from 'boring-router-react'; import {observer} from 'mobx-react'; import React, {Component} from 'react'; Const history = new BrowserHistory(); // Create router object const router = new router (history); Const route = router.$route({account: About: true, notFound: {// $match can be a fixed string or regular expression. By default, $match is the key of the route. RouteMatch.rest, }, }); @observer class App extends Component {render() {return (<> {/* Uses the Route Component's match property to match a particular Route */} <Route Match ={route.account}> account page <hr /> {/* Use the to attribute of the Link component to specify the Link address */} <Link to={route.about}> about </Link> </ route >  <Route match={route.about}>About page</Route> <Route match={route.notFound}>Not found</Route> </> ); }}Copy the code

Router.$route() creates a route attribute and its subattributes as an Observable. In the case of Route. about, the route.about.$matched property changes based on the current route’s matching status, while the Route component renders or ignores the component content with the $matched value of the incoming route entry.

This means that, except for the Route component, we can use the Route object directly anywhere to determine a match or to read the parameter information. This is one of the biggest differences between the Boring Router and routers like the React Router: The Boring Router has centralized and exposed route state management.

Now that you’ve looked at the faces, I’ll cover the Boring Router’s features.

Type safety

The Boring Router is developed in TypeScript, and the API is designed with type-safety as a priority (which of course brings with it some usage compromises). The subitem and parameter types (including path parameters and query parameters) are computed from the object literal types defined by the route.

When using routing, you don’t need to use strings directly in almost all scenarios. In addition to the front end, it can also be used on the back end by sharing the route definition to build the path string at run time from the route object:

import {Router, ReadOnlyHistory} from 'boring-router';
import {routeSchema} from 'shared';

const history = new ReadOnlyHistory();

export const router = new Router(history);
export const route = router.$route(routeSchema);
Copy the code

To generate a path string, call the route object’s $href() method:

let path = route.about.$href();
Copy the code

This way, when there are path updates, you don’t have to worry about fixing the missing places.

Parallel routing

When a page consists of multiple parallel views that are relatively independent, common routing does not seem to provide the ability to express the paths of different views separately. Take the Modal Gallery in the React Router document example as an example. When dealing with parallel views, extra state needs to be carefully maintained, in other words, it does not directly provide corresponding support.

The Boring Router has added “parallel routing” support for this parallel view:

const router = new Router<'sidebar' | 'overlay'>(history);

/ / the main road
const route = router.$route({/ *... * /});

// Sidebar routing
const sidebarRoute = router.$route('sidebar', {/ *... * /});

// Overlay routing
const overlayRoute = router.$route('overlay', {/ *... * /});
Copy the code

The use of parallel routes is basically the same as that of primary routes, except that the query parameters (? Foo =bar) needs to be shared with the main route. In the address bar, the path of a parallel route is represented by a group name starting with an _, such as? _sidebar = / foo & _overlay = / bar. For example, in the Makeflow project we commonly use parallel routing:

In an interesting application scenario, our current message notification stores the ref(route.xxx.$ref()) of the view’s route in the message if it opens a particular view/page. In the sidebar view, for example, the ref stored in the message is? _sidebar = / XXX. The front end just needs router.$push(ref) to open the message. Because the ref here does not start with a /, the Boring Router assumes that it contains only the parallel routing part and does not change the main routing path of the current page.

The life cycle

We put a lot of effort into self-implementing the BrowserHistory object in the Boring Router, which tracks page navigation and enables historical state recovery (not just the current path, but also history).

Thanks to this, the Boring Router provides a richer lifecycle API:

  • $beforeEnter/Update,$afterEnter/Update(Asynchronous)
  • $beforeLeave,$afterLeave.

In the before* hook, you can terminate a jump or redirect to another route at any time:

route.xxx.$beforeEnter((a)= > {
  if (Math.random() < 0.5) { route.yyy.$replace(); }}); route.xxx.$beforeLeave((a)= > Math.random() < 0.5);
Copy the code

To facilitate reuse and organization of lifecycle hooks, the Boring Router provides “routing services” that can be defined through route.xxx.$service(). Through the routing service, parameters can be converted so that the view can directly use the transformed object. For details, see the routing service example.


That’s it. If you’re working on a new project that uses MobX + React + TypeScript, the Boring Router could be a Boring Router to tread on. If there is any question in the use of 🙂, you can raise an issue, and I will answer it in the first time.