This is my entry for the F# Advent Calendar 2021. Thank you Sergey Tihon for organizing it. This post will explore building a small demo application using the Sutil framework. See the final result here.
When evaluating a new framework for building interactive web apps, it's always difficult to get an actual feel for how the framework works from just reading the docs. Eventually people came up with the the TodoMVC page to compare a Todo list built with various frameworks. This allows developers to evaluate a potential framework before committing to it by seeing if someone else has implemented this Todo demo with that framework, and if not, then by contributing to the project themselves.
However, in my opinion, this Todo list is too small of a demo application to really get a good feel and thorough understanding of how a framework works, what shortcomings it may have, as well as how it interacts with other libraries that one might need. This is where the RealWorld project comes into play. It has a similar goal of comparing frameworks by building the same demo application, but the demo app it champions is something that is much closer to what someone might build in the "real world". This app is called Conduit.
Conduit is a small blogging platform that allows visitors to read blog posts by various authors, and optionally create an account to write their own posts. With an account, users may also follow other authors, favorite posts, and leave comments on other blog posts. This is the "real world" application that gets built by different developers using different frameworks to compare them, and they all target the same backend that is provided by the RealWorld project.
This application comes much closer to how one might use a framework to build an application at is has the most common requirements, such as form inputs, reusable components, routing, authentication, and interacting with a REST API. That is why I believe that it is a much better project for comparing frameworks, and this is also the application I chose to build using Sutil.
Sutil is a web framework implemented in pure F# with a minimal runtime library. It's heavily inspired by Svelte if you're familiar with that. The main aim was to build a framework from scratch in pure F# that doesn't rely on an existing JS framework during runtime. It accomplishes this mainly with the use of the observable pattern and the .Net
IObservable interface. It utilizes a Feliz style DSL to write views. It has a reactive system to track state updates, and exposes a system it calls
Store to handle local component state. When updating a store value, Sutil will only rerender the part of the view that explicitly references the store value, meaning there's no VDOM between Sutil and your DOM.
TL;DR Sutil is ...strange, but nice.
At the time of development, Sutil had just hit version 1.0, and documentation was still a work in progress. However, both the framework as well as the documentation and community show a lot of promise, and I'm excited for how it will pan out in the future. I believe Sutil has the potential to become something great.
It's incredibly easy to get started with Sutil, and easy to understand how to create a store value and use it. Just check out this earlier F# advent post. The stores can be either scoped to the local component, or reference an Elmish store to share state. Additionally, the Feliz DSL for writing views is very intuitive and keeps your markup type safe. The fact that it doesn't rely on any other 3rd party library means that you only have to learn how to use Sutil, and nothing else. Whereas with e.g. Fable.React, you'd have to learn both ReactJS, as well as how Fable.React wants to interact with it. This extra overhead doesn't exist with Sutil, which is really nice. This means that when you're building a web app, you're simply writing F#, and it feels it too.
Architecting your application also feels simple, and is completely up to how you want to do it. There's no convention forced upon the developer that certain components must go in a specific location. This means that it's simple to organize components in such a way to promote reuse, and using them within other components is intuitive.
Everything revolves around a
Store, and initializing a store value give you a,
IObservable<'a>. This is also the core of how Sutil knows what parts of the view to update. Given:
let name = Store.make "World" Html.div [ Html.span [ text "Hello: " ] Bind.el(name, fun n -> Html.span [ text n ]) ]
name store has its value updated, then Sutil will only rerender the parts inside the anonymous function. This is also the preferred way of getting at the value of the store. This is simple in concept, but when you start having complex objects it can get complicated at times. This gets especially funky when your store model contains a list of objects, each with their own lists. e.g. A list of Posts, each with a list of Tags as is the case with Conduit. At this point there are several options. A simple
for number in [1..9] do works, but is static, and won't rerender if the collection updates. However, it gets confusing when you are binding a parent value, and then want to reiterate over the child collection, in which case a
for loop does suffice as the entire block inside the parent object binding will get rerendered.
According to the documentation, there are several ways of dealing with collections, such as specific
Bind.each functions, as well as variations that track index, as well as the possibility of providing a key. Using a combination of these functions is required for optimal performance, but it isn't always clear which one is the most appropriate option as they all can work.
In a similar vain, trying to render an element that references several stores simultaneously can also get hairy. This requires reacting to several observable state at the same time, and therefore care must be taken to ensure correct bindings. In situations like these, and above, Sutil allows for creation of new
Stores that are combinations or subvalues of a parent store. The more I worked with Sutil, the more I realized that the framework prefers having a large amount of tiny stores that hold the final wanted value and nothing else.
There are also times where the various Store functions can get rather confusing. Depending on if you want to subscribe to changes, filter on a store value, or get values from a store, or a passed in Elmish model, you may get an
IStore as a new value, or
IObservable instead. In most cases, these are interchangeable for rendering in the view, but there still exist a few locations where they're not, at which point it becomes increasingly difficult to get at the desired value.
Among one of the nice functions that Sutil provides is an
ObservablePromise which allows automatic subscription of promise values, and conditionally rendering either a pending, error, or completed value. This works nicely, until your function that return a promise itself could error out, at which point it's not clear how to map that error into the Sutil
This leads into perhaps my biggest gripe with Sutil, which is when an exception does happen within the returned view, nothing happens. Meaning there's no rendering happening for that specific view function, but no exception gets propagated upwards. The one thing that does happen is that there is a
console.log message, but it doesn't state where the exception happened, nor does it provide any other information. It simply states that a binding error happened somewhere. This makes it increasingly difficult to actually pinpoint where your code needs work, as even classic
try...with statements don't work.
Sutil is not without problems, but that's to be expected as it's still early in development. Some of the confusing parts are very confusing and could be helped with decent documentation. I'll be looking forward to how the framework evolves further, but until then, I might try out something else.
It's still missing the commenting system from the RealWorld project, so it's not yet complete. Additionally, Sutil provides some simple functions for working with the URL, but nothing like a complete Router. I wonder if that's within scope of the project to include a full router, or only the primitives to allow people to build their own. But in SubtleConduit, I opted for Navigo with the help of this repository. Using Navigo worked, but introduced a lot of complexity and its own set of problems.
I used Vite as the bundler, which ended up working incredibly well. It required no configuration, and was able to build only the parts that change in a dev server, and automatically supply correct source maps.
Lastly, I used the CSS Type Provider combined with TailwindCSS which also worked really well. However I needed to supply a separate config to the type provider than was used in the actually web app, as the type provider can't work with the Tailwind JIT mode. This also means it won't work with Tailwind v3 at all until something else happens.
Overall I was very happy with the tech stack and enjoyed working with the individual components