How to tame Mithril’s router and write clear apps

I moved to Mithril and I seri­ous­ly love it! Sad­ly, the router alone has been enough of a pain point to make me rethink how to use Mithril. I will use pro­fan­i­ty on the sub­ject as a cop­ing mech­a­nism while I write, but don’t let that fool you into think­ing that I blame any­one but myself for any bad code I write, or that I reserve any­thing oth­er than respect for Mithril’s patient and knowl­edge­able main­tain­ers.

If you don’t know what a router is or does, don’t wor­ry. I wrote a sec­tion to get you up to speed.

For those of you new to Mithril, I want to flat­ten your learn­ing curve for the v1.1.x Mithril router and then show you how tam­ing the router tames your Mithril apps. Every­thing I will show you can already be found in the doc­u­men­ta­tion, but I read the docs and still made “gotcha” mis­takes that didn’t reg­is­ter until they hap­pened. My hope is that by para­phras­ing the docs in terms of the router’s quirks, oth­ers won’t have as frus­trat­ing of an expe­ri­ence. For that rea­son, if you are already famil­iar with sin­gle-page appli­ca­tion (SPA) routers this arti­cle will still help you avoid paint­ing your­self into some cor­ners.

Once we’re squared away on the router I will show you how to clean­ly sep­a­rate rout­ing pol­i­cy to make your app eas­i­er to change. Final­ly, I’ll show you one demo Mithril appli­ca­tion orga­nized in this way.

I assume you are either curi­ous about get­ting start­ed with Mithril or have used it just enough to won­der how to improve your use of it. I also assume that you know how to write Mithril com­po­nents.

Preamble for routing newbies

If you’ve used a SPA router before, skip this sec­tion.

A SPA router takes a path you type into the address bar and nav­i­gates the end-user to a spe­cif­ic spot in your app. Unlike how nav­i­ga­tion tra­di­tion­al­ly works, SPA routers do not refresh the brows­er when the user moves about. This makes web appli­ca­tions feel more cohe­sive and flu­id. A route might look like /pages/home or #!/shop/checkout, depend­ing on what part of the web address the router uses to track the user’s loca­tion.

Typ­i­cal­ly routers want you to set them up by telling them all the places the user can go, along with what hap­pens to the user at each of those places. This can be done in sev­er­al ways. Some­times you give a router a big object (some­times called a rout­ing table) with the routes as keys and some imple­men­ta­tion-spe­cif­ic type as val­ues. Rout­ing tables typ­i­cal­ly show clear intent.

    '/': HomePage,
    '/about': CorporateInfo,
    '/shop': Storefront,
    '/support': HelpDesk,

Oth­er routers might give you a dec­o­ra­tor func­tion that mark oth­er func­tions to han­dle spe­cif­ic routes. The premise is the same.

function HomePage() {
  // ...

function CorporateInfo() {
  // ...

You use routers to map out your appli­ca­tion from a navigator’s per­spec­tive. Routers tend to have oth­er fea­tures, such as pars­ing val­ues from routes as func­tion argu­ments, or his­to­ry manip­u­la­tion. The Mithril router is just one of many vari­ants out there.

Mithril’s Router

You sum­mon Mithril’s router using the m.route() func­tion, a rout­ing table, a DOM node that will hold ren­dered con­tent, and a default route.

See the Pen Sim­ple Mithril router exam­ple by Sage Ger­ard (@zyrolasting) on Code­Pen.0

Each route can be asso­ci­at­ed with either a Mithril com­po­nent or a RouteRe­solver object (dis­cussed lat­er) that com­mands more refined con­trol. In the above exam­ple, I use a Mithril com­po­nent that will print Hello, Sage! when you vis­it the /hello/sage route. You can see that the name was parsed out of the route itself by Mithril.

In your code, you can tell the router to nav­i­gate some­where using m.route.set(). You can ask for the last resolved route using m.route.get(). You can parse our vari­ables in your route using m.route.param().

For these sim­ple cas­es, that’s all you need to know. But I want to jump right into the gotchas lest you end up with a false sense of secu­ri­ty.

Mithril resolves routes on its own schedule

The Mithril router resolves routes asyn­chro­nous­ly. Say if you are on a /cart page and you want to go to your pro­file page at /profile. You might use m.route.set() to nav­i­gate. But if your code then asks Mithril’s router for the cur­rent loca­tion with m.route.get(), your state is going to be wrong.

See the Pen Mithril router async res­o­lu­tion by Sage Ger­ard (@zyrolasting) on Code­Pen.0

The above exam­ple shows a view that cor­rect­ly prints the active route, but the console.log shown in the exam­ple says the route is undefined right after m.route.set() kicks off nav­i­ga­tion. That’s because m.route.get() only tells you the last resolved route, which can lead to con­fus­ing bugs if you use m.route.get() to update a view mod­el at a bad time.

To expand on this point, did you ever make a view that looks like it has data from one page “ago”? If so, then you used m.route.get() or m.route.param() to change state out of sync with the Mithril router. In some cas­es, you want the pend­ing route, that is, the route that the router is cur­rent­ly resolv­ing when you are try­ing to sync up your app state to router behav­ior. The com­po­nent in the above exam­ple doesn’t have this prob­lem because by the time it ren­ders the Mithril router has already deemed the route “resolved” so that m.route.get() and m.route.param() deliv­er the lat­est data.

Mithril has no gimme-the-pend­ing-route API, and you can’t use window.location with­out track­ing the router’s pre­fix (like #!)—which also lacks a get­ter. You can only set the pre­fix using m.route.prefix(). Mithril is basi­cal­ly clam­ming up so that you have to solve your rout­ing needs with your state. The only way to reli­ably access and use the pend­ing route for track­ing pur­pos­es is by writ­ing RouteRe­solvers.

RouteResolvers: To block, or not to block?

When Mithril match­es a route, it will start ren­der­ing once you give it a com­po­nent. In this exam­ple, a Mithril com­po­nent ren­ders out­right and redraws implic­it­ly as per the default behav­ior of m.request().

See the Pen Mithril Rout­ing — Load data with­out block­ing by Sage Ger­ard (@zyrolasting) on Code­Pen.0

This approach is easy enough, but if the net­work fetch fin­ish­es quick­ly, the load­ing indi­ca­tor may look like an unpleas­ant flick­er. To fix this, the docs sug­gest block­ing ren­der­ing using a RouteRe­solver, which expos­es the pend­ing route as dis­cussed ear­li­er. But I won’t show that here because I instead want you to under­stand how RouteRe­solvers change the flow of route res­o­lu­tion. All you need to know is that your pend­ing route info is in the argu­ments to the onmatch() func­tion (which fires when, guess what, the asso­ci­at­ed route match­es).

See the Pen Mithril Rout­ing — Load data with block­ing by Sage Ger­ard (@zyrolasting) on Code­Pen.0

This exam­ple removes the flick­er by remov­ing the load­ing indi­ca­tor entire­ly. The doc­u­men­ta­tion acts like the scorched-earth approach is a sell­ing point, but tell that to users with bad con­nec­tions. So long as you return a Promise, that onmatch() can be used to delay ren­der­ing until data is avail­able for a com­po­nent. Reject­ing the promise makes Mithril fall back to the default route. A more robust approach would always allow for an indi­ca­tor to account for slow­er con­nec­tions while pre­vent­ing flick­er. You can also use RouteRe­solvers to opti­mize lay­out diff­ing using an option­al render() func­tion. For now both tech­niques are left as an exer­cise to the read­er, but if you ask me, nev­er block ren­der­ing and be smart about load­ing indi­ca­tors.

We now know enough to make a mess.

Introducing policy

Assume you are work­ing on an admin dash­board for “ten­ants” that make wid­gets. We need:

  • A wid­get list­ing page for one ten­ant. Authen­ti­cat­ed users only.
  • A wid­get detail page. Authen­ti­cat­ed users only.
  • An error page to dis­play prob­lems load­ing data, and
  • A login page.

I call the deci­sions on how to nav­i­gate and han­dle relat­ed prob­lems the rout­ing pol­i­cy because they are nav­i­ga­tion­al require­ments no mat­ter what. Here’s a Mithril router con­fig­u­ra­tion that meets these require­ments.

m.route(root, '/login', {
  '/login': Login,
  '/error': ErrorReport,
  '/:tenant/widgets': {
    oninit() {
      if (!app.user) {
      } else {
        app.tenants[m.route.param('tenant')].loadWidgets().catch((err) => {
          m.route.set('/error', null, {err});
    view() {
      const {widgets} = app.tenants[m.route.param('tenant')];

      return (widgets)
        ? m(WidgetsOverview, widgets)
        : m('p', 'Loading...');
  '/:tenant/widgets/:id': {
    onmatch({tenant, id}) {
      if (!app.user) {
        return Promise.reject();

      return app.tenants[tenant].loadWidgetDetail(id)
      .then((detail) => {
        return {view: () => m(WidgetDetail, detail)};
      .catch((err) => {
        m.route.set('/error', null, {err});

The pol­i­cy says that guests are redi­rect­ed to /login if they request a pri­vate route. To do this, the route han­dlers do dif­fer­ent things under the assump­tion the app state is in scope.

  • The com­po­nent attached to /:tenant/widgets redi­rects the user in oninit()
  • The RouteRe­solver attached to /:tenant/widgets/id returns a reject­ed Promise.

The lat­ter case caus­es Mithril to fall back to the default route, which is /login in this case. This implic­it­ly pro­grams the same behav­ior. You could still call m.route.set() in the RouteRe­solver and return undefined assum­ing there isn’t a ren­der pass that would be sur­prised by that.


I inten­tion­al­ly left out error han­dling and the ori­gin of app for brevi­ty, but in the shoes of a junior who has nev­er seen a router or Mithril before, does this code make any god­damn sense?

Look at how much stuff we have to know just to write code that block guests. If one flow blocks ren­der­ing and the oth­er doesn’t, our pol­i­cy shouldn’t have to change shape to accom­mo­date in this case.

You might (cor­rect­ly) argue that this case is easy to refac­tor into a more con­sis­tent form. Incon­sis­ten­cy in how we resolve routes is not Mithril’s fault. How­ev­er, Mithril does oblige us to write our rout­ing pol­i­cy in its router’s terms. Once rout­ing tables get large and you have juniors on your team, keep­ing order will become a chore. For exam­ple, to bring back a load­ing indi­ca­tor in /:tenant/widgets/:id (Prod­uct will ask for it; don’t act like they won’t), you have to either:

  1. Replace the RouteRe­solver with a com­po­nent to ren­der that indi­ca­tor imme­di­ate­ly; or
  2. Make onmatch return a com­po­nent imme­di­ate­ly, out of sync with the loadWidgetDetail call.

That’s not obvi­ous to every­one. And if you are from the “pass state down as attrs” school of thought, you can get fucked because chang­ing around Mithril’s rout­ing code means you have to rewire how state cir­cu­lates to your com­po­nents (No, I will not import my app state in every JS mod­ule I write. That’s a dif­fer­ent arti­cle). It’s easy for a team to end up in an awk­ward posi­tion where sim­ple changes lead to non-triv­ial refac­tor jobs.

Let’s make this more inter­est­ing: What do you do if you want sev­er­al load­ing indi­ca­tors start­ing with a big spin­ner, then a dash­board break­down with a bunch of lit­tle baby spin­ners on ana­lyt­ics wid­gets crunch­ing num­bers? The answer is not worth think­ing about, but I can tell you my first attempt involved an unholy union of dec­o­rat­ed RouteRe­solvers.

Organizing code around policy

Again, the strug­gles with Mithril’s router is not a state­ment on its qual­i­ty. How­ev­er, some­thing is back­wards. Our rout­ing pol­i­cy should not depend on Mithril’s router. The oppo­site must be true to clean up our code. From the Bob Mar­tin school of thought, it is the rules that make an app work that should sit at the cen­ter of your soft­ware. While we can­not phys­i­cal­ly force Mithril to depend on our code, we can reor­ga­nize to say that the rules of our app dic­tate every­thing from rout­ing, to state, to views, and so on.

After writ­ing and rewrit­ing my rout­ing code sev­er­al times to do what should be sim­ple things, I asked myself what I want­ed my code to look like, with this in mind.

For afore­men­tioned rea­sons I knew that the code I want­ed would have to meet the fol­low­ing require­ments:

  • The code says what it does and does what it says.
  • You should be able to change rout­ing pol­i­cy with­out need­ing 6 can­dles, a virgin’s sharp­ened femur, and a sheep.
  • Appli­ca­tion state is more author­i­ta­tive than the router’s state.
  • Nev­er block ren­der­ing. Views ren­der the app at any moment. If the views look wrong, I either changed state wrong or redrew at a bad time.
  • The Mithril com­po­nents are pure (attrs return con­tent deter­min­is­ti­cal­ly).

I came up with this:

dispatch(document.getElementById('root'), '/error', app, {
    '/': () => Login,
    '/login': () => Login,
    '/error': () => ErrorReport,
    '/:tenant/widgets': loggedin((app, task) => {
        const {tenant} = app.spaRequest;

        task(() =>
               .then(() => app.loadWidgetListing()))

        return WidgetsOverview;
    '/:tenant/widgets/:id': loggedin((app, task) => {
        const {id, tenant} = app.spaRequest;

        task(() =>
               .then(() => app.loadWidget(id)));

        return WidgetDetail;

dispatch() also sets up the Mithril router, but you have to inter­pret the set­up dif­fer­ent­ly. It assumes that all state comes from one place, à la Redux. That’s where you see app passed in. It can be a plain old emp­ty object if you want. It also assumes that the pend­ing route and all relat­ed argu­ments are part of appli­ca­tion state, and makes it avail­able as part of app.spaRequest.

Sec­ond, the /error route spec­i­fied is not just a default route. It is con­sid­ered the route you vis­it in error con­di­tions, and I treat it that way for the same rea­son I would write try..catch in an entry point.

Final­ly, the rout­ing table itself is a table of routes to func­tions. The func­tions return com­po­nents to ren­der syn­chro­nous­ly but may change state asyn­chro­nous­ly in terms of the pend­ing route using a task() func­tion. The task func­tion takes a func­tion that returns a Promise and redi­rects to the error route in the event of an unhan­dled fail­ure. Oth­er­wise, it starts a new ren­der pass when the task fin­ish­es.

That loggedin() func­tion you see is just an app-spe­cif­ic dec­o­ra­tor that redi­rects users to /login if an authen­ti­cat­ed user is not in app state. You will see it in the below Code­Pen. I want you to see that as an exam­ple of why it should be easy to express rout­ing pol­i­cy. Pre­vi­ous­ly pol­i­cy had to be parsed from Mithril seman­tics, but in this case I could remove Mithril from my project entire­ly and not vio­late the rules as gov­erned by this approach. If you want to guard a route from unau­then­ti­cat­ed users, slap loggedin on it. If you want to add a colony of nest­ed load­ing indi­ca­tors, do that in your com­po­nent, then run task calls that iter­a­tive­ly update state with data in stages.

Some might also want to fac­tor out the “authen­ti­cat­ed ten­ant selec­tion” pat­tern in the han­dlers for clean­li­ness. That is now an easy change, and you can clear­ly see what guar­an­tees are made about state with func­tion dec­o­ra­tors, even if the flow is high­ly sub­jec­tive.

function tenantRoute(fn) {
  return loggedin((app, task) => {
    const tenantTask = (next) => task(() => 

    return fn(app, tenantTask);

dispatch(document.getElementById('root'), '/error', app, {
    '/': () => Login,
    '/login': () => Login,
    '/error': () => ErrorReport,
    '/:tenant/widgets': tenantRoute((app, tenantTask) => {
        tenantTask(() => app.loadWidgetListing()))

        return WidgetsOverview;
    '/:tenant/widgets/:id': tenantRoute((app, tenantTask) => {
        tenantTask(() => app.loadWidget(;

        return WidgetDetail;

This approach is not with­out caveats. For one, you are oblig­ed to write LBYL-style com­po­nents that check for incom­plete state to ren­der one of many pos­si­ble vari­a­tions of a view. Sec­ond, you have to be extra care­ful to syn­chro­nize state between redraws. This should go with­out say­ing, but the approach in dispatch() makes it you to spawn con­cur­rent Promise chains that all mutate state. But those of a Redux mind­set would see an oppor­tu­ni­ty to extend task() with the trans­ac­tion­al behav­ior expect­ed from reduc­ers.

Demo, plus other considerations

If you want to see an exam­ple of dispatch() in action, play with this SPA.

See the Pen Con­trived dis­patch() demo by Sage Ger­ard (@zyrolasting) on Code­Pen.0

The Code­Pen con­tains the tran­spiled demo from the mithril-dis­patch GitHub project, which uses a rout­ing pol­i­cy based on a real-world SaaS shop despite the sim­plis­tic con­tent. I encour­age you to start read­ing here to see the code orga­ni­za­tion ben­e­fits brought by dispatch(). As a bonus, the demo also shows a high­ly-opin­ion­at­ed use of lay­out diff­ing opti­miza­tion for users famil­iar with RouteResolver.render(). You can also see that the com­po­nents end up pure despite com­plex, slow oper­a­tions that would involve heavy use of life­cy­cle meth­ods oth­er­wise.


In this arti­cle I have intro­duced you to SPA routers. From there, we learned about Mithril’s router and how it han­dles dif­fer­ent pol­i­cy deci­sions when rout­ing. We learned that chang­ing the rout­ing pol­i­cy to meet sim­ple require­ments is hard­er than just express­ing the pol­i­cy in plain lan­guage. By adding a dispatch() func­tion to sep­a­rate pol­i­cy from Mithril seman­tics, the Mithril com­po­nents we write only ren­der what they are giv­en, state con­cerns itself with the data, and route res­o­lu­tion mere­ly con­nects the two togeth­er.

I also empha­sized that more than any­thing, the rules of your app are king. Just because I wrote dispatch() in the man­ner shown does not mean I will use that exact imple­men­ta­tion lat­er. Just be sure that you do not make your rules depend on Mithril’s router, or even Mithril in gen­er­al. Frame­works are meant to enable your pro­duc­tiv­i­ty, not tell you how to code.

I love cod­ing and have done so pro­fes­sion­al­ly for over a decade. I use what lit­tle free time I have to exper­i­ment on ideas of var­i­ous qual­i­ty and to help oth­ers write qual­i­ty soft­ware. If this post was help­ful to you, please leave a tip and share this arti­cle with any­one that need to tame the Mithril router.

Do NOT follow this link or you will be banned from the site!