Navigation with Fable.Elmish

This post is my contribution to the 5th #FsAdvent calendar. Thanks to everyone involved, you make the F# world better.


F# allows to develop not only server applications, but also front-end side with the help of Fable (F# to JavaScript) compiler. Besides the compilation itself, you, as a front-end developer, need to choose the architecture of your client-side application (of course if you don’t want to have just a couple of JS animations on a static page). In the Fable world, the most popular approach is to utilize MVU (Model-View-Update) architecture, borrowed from the Elm language. F# implementation of MVU pattern is called Elmish, where ReactJS (fable-react) is commonly used for View part.

Elmish provides several helpers for building large-scale single-page applications, including not only Model-Update lifecycle, but also navigation APIs for browsers. In this article I would like to go through the possible types of Elmish navigation.

Nowadays modern browsers support two types of routing:

  • Hash-based (when SPA handles the changes only in the hash part of the URL)
  • Push-state (when SPA handles the changes in the whole path of the URL thanks to window.history API)

Fable.Elmish supports both of them.

Hash-based routing

This type of routing is supported even with the oldest browsers, since only the hash part of the URL is being changed; and initially hash was used for anchor links on the same page. It can implemented easily with UrlParser.parseHash helper. The simplest example we could even imagine:

We have a tiny application, with a model, containing only the current page. Depending on the selected page we show either “Home page” or “About page” text. To update the model with the current page, we rely on parseHash helper, but to handle the mapping, we need to provide a parser (pageParser). In our case it has only basic string mappings to home and about terms with a fallback to home for root(/) path. When a parser finds the appropriate route, it passes the page to our urlUpdate function, where we finally update the model if the URL matches some route, otherwise we modify the URL to fallback to Home page. At this point we can get the arguments (path, query string). We also need to implement init function with a parser result argument, so we could update the model with the current page at the initialization stage.

Note that this application is totally simplified, it doesn’t even have messages, so the update function doesn’t do anything, just returns the same model.

Hash-based routing meets all requirements of any possible SPA. With it you just use links with hashes, and Elmish subscribes to hash changes for the navigation. This approach is handy, but I would say, hash is a bit outdated, URLs without it look much better.

Push-state routing

Push-state routing is visually much nicer. URLs look like regular links with a path, but there is no full reload of a page. However, in Elmish it requires manual control on the links. Elmish.Navigation has an event 
let [<Literal>] internal NavigatedEvent = "NavigatedEvent". Unfortunately, it is internal, but we can use the string to cheat a bit. ;)

So let’s implement a wrapper element, which we can use to trigger this event.

First of all we create LinkProp type with necessary properties:

  • To — for replacement of Href property
  • Replace — if we want to replace the current history state instead of pushing a new one (Replace false)

Secondly, we implement the same not as a discriminated union, but as an interface, to be able to use the props as an strongly typed object.

Then, we implement the link itself. It is just a function with two arguments: props and children. It wraps the passed children with an a element, but with handling its OnClick event. In the handler, we check:
if the user passed custom OnClick handler, just invoke it;
if not, we check that defaultPrevented = false, the click was not modified by any button, such as ctrl, alt, shift or meta key, that the link doesn’t have Target "_blank" and the link is clicked only with the left mouse button.

If all conditions are met, we can be sure that it is a standard link click. So we prevent default, depending on Replace property we push or replace the history state and then we trigger the "NavigatedEvent", which will be handled by Elmish.

We also want to pass the rest of properties to the a element, so we filter out LinkProp and HTMLAttr.Href props and concat the list with our OnClick and Href props.

For push-state routing, Elmish app implementation will be pretty much the same, except that you need to use parsePath instead of parseHash and our custom link instead of just a.

Wrapping up

Comparing the above mentioned types of routing, I, personally, prefer the latter, since it is more modern, using the latest history API and with better URL look. There are a couple of drawbacks, of course: browser compatibility and manual handling of the links, which is a bit cheaty in the provided implementation. However, I believe, that Elmish is stable enough and we can make the NavigatedEvent public.

P.S. The implementation of link element is inspired by react-router project, which has almost the same approach.