Skip to content


Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

Type-Safe SPA

This is a small demo that explores different approaches for creating a potentially scalable single page application. See here for the completely unspectacular sample apps in action, and here for my conclusion.




  • There is a global session object that potentially can hold shared state for all parts of the application.
  • The login page authenticates the user and initates the creation of the session.
  • After logging in, the user is redirected to the dashboard, where they are welcomed by name.
  • There is an additional profile page, showing the name again.
  • From the profile page it's possible to go to an "edit profile" page that allows changing that name.
  • After the name has changed, that change must be reflected in all parts of the application: dashboard, and profile page.
  • Although "edit profile" could be seen as a child of "profile", it is kept on the same level for simplicity reasons (we potentially already are at the third level here).


That scope of functionality allows us to look at the following questions:

  1. How are parent-child relationships handled?
  2. How much boilerplate code needs to be written?
  3. How much guidance and support provides the compiler?
  4. How maintainable, aka scalable, does the approach appear to be?


Elmish + Feliz


  • When introducing intents (aka external messages), the parent doesn't know about them until we start handling them there. So the compiler wouldn't complain if we forget to add it to the parent(s).
  • Routing can fail silently when the expected route isn't found. Sometimes that's just a matter of lower and upper case letters.
  • Feliz.Router is best being used with arrays representing the url segments, because that's what can be passed to the format() function (it does not accept lists).
  • The Feliz syntax felt rather verbose at first. Unfortunately, Html is a type and not a module and therefore "cannot be opened". However, in direct comparison to the "classic" Elmish view syntax, for real world tasks there seems not to be that much of a difference in terms of the number of written lines of code. Good old HTML will in most cases still stay shorter, though...
  • Except for commands fully testable, as everything is (mostly pure) F# functions
  • Does not require much knowledge of React

React Function Components + Feliz


  • Much less boilerplate code to write in comparison to Elmish + Feliz
  • It is basically React written with F#, so React knowledge is needed
  • In addition, regular updates are needed whenever React changes
  • Working with the context API of react needs getting used to, but is then relatively straightforward
  • Passing data to components through props is straightforward as well
  • useElmish is useful for more complex forms (see login and edit profile), but simple operations can also be handled more pragmatically (see logout)
  • Right now, Hot Module Reloading (or Fast Refresh) does not work. Which is a clear obstacle during development time.
  • TBD: Testability

React + TypeScript


  • Having not actively worked with TypeScript since its early days in 2012, it was surprisingly easy to get started.
  • Although this small demo doesn't use much of the language's features, those that are being used were really easy to pick up and a pleasure to work with.
  • The tooling based on Visual Studio Code is amazing. I not only like the automatic code-formatting but also the really fast feedback loop that is provided during development time.
  • When the build breaks, it does so within milliseconds, and error messages are always helpful. Most of the time, however, it already becomes obvious that something went wrong while typing.
  • I could take over all the concepts (components, context, ...) from the previous implementation with Reaction Function Components + Feliz, which allowed me to implement the TypeScript demo within a couple of hours.
  • I decided to implement the two parts of the app which contain a bit more "logic" (login + edit user name) without following any strict pattern like MVU. As everything is self-contained in its own component, this seems reasonable.
  • react-router is powerful but relatively easy to get started with.
  • As with the Feliz sample, function components are easy to reason about.


Lines of Code

Although it is not the most important factor, it might be interesting to see how much more or less code every approach produces. This comparison was created through cloc and only counts those files that are actually used to write the app. So the compiled JavaScript, for example, is not considered.

Approach App Files Blanks Comments Code
Function Components + Feliz 13 59 6 316
Elmish + Feliz 11 77 6 404
React + TypeScript 11 27 1 241

Performance and bundle size

The following is measured by looking at the final and production-ready compile SPAs with Google Chrome (83) on macOS. It is obviously not objective and may be improved by some settings (I just used the default configuration as provided by the templates I used). But it gives an idea about what to expect.

Approach Requests Transferred Resources DOMContentLoaded Load
Function Components + Feliz 8 253 KB 466 KB 167 ms 166 ms
Elmish + Feliz 7 253 KB 468 KB 171 ms 173 ms
React + TypeScript 7 216 KB 328 KB 131 ms 132 ms

(Transferred = compressed, Resources = uncompressed; in each case the fastest run was chosen)