DEV Community

Cover image for Exploring The F# Frontend Landscape
Angel Daniel Munoz Gonzalez
Angel Daniel Munoz Gonzalez

Posted on • Updated on • Originally published at blog.tunaxor.me

Exploring The F# Frontend Landscape

Hello everyone it's been a while!
Today we will talk about what is the current frontend landscape of Frontend development for the F# ecosystem. Over the last few years Fable has become quite capable and more bindings have been released.

Disclaimer: The F# Ecosystem Is Stable you don't need to jump ship to the next thing or anything like that! Just because there are options doesn't mean you need to ditch out what you know and learn something new. Think of this as as restaurant menu: There are options but ultimately it is your choice which one you pick and you can even decide "I don't want to eat here" it's completely fine. No kittens will die and the world won't stop so if you see a lot of options I'd suggest you to not feel pressured to choose!

When it comes to the Frontend landscape for F# we have three main roads:

  • Fable
  • WebSharper
  • Web Assembly

These have different approaches to the frontend landscape but ultimately do the same thing Websites some of those options allow you to go for server side rendering and others allow you to go for single page applications.

Fable

In this Section:

High Profile:

  • Feliz
  • Fable.Lit
  • Sutil

Low Profile

  • Fable.Svelte
  • Feliz.Snabdom

Fable Next:

  • Feliz.Solid

Fable is an F# to JavaScript compiler (like Typescript compiles to JavaScript) it is near its fourth major release and has a very very strong ecosystem based around React.js although, in recent times other options have become available.

Why Fable and who benefits most from it?

Fable is meant for those developers that need to work with the JavaScript ecosystem or that want to benefit from it, it would be unwise to negate the number of good libraries and existing solutions that have been born in it.

Fable rather than negate that JavaScript exists, builds on top of it and gives you the most flexible option when it comes to frontend development in F#.
You keep using the safety of F# (most of the time) and when necessary you can fall back to JavaScript interoperation (via emit, imports or the dynamic operator) or even JavaScript itself to fill the missing gaps that could be there.

The bad thing is that the JavaScript ecosystem is so vast and has grown so much over the years it might be possible that if you want X library there won't be bindings for it. After all the F# developer numbers are way too low compared so we don't have the programmer power to be on equal grounds.

Writing bindings isn't complex but it can take quite some time from your development efforts if the libraries you're targeting are too big, this cost is only paid once though, when the bindings are complete is just a matter of maintaining the bindings up to date (which isn't too big of a chore).

That being said! Let's dive into Fable's options:

Feliz

This is by far the most popular library in the F# frontend ecosystem, This library took existing lessons from Fable React and improved its DSL (Domain Specific Language) to be more concise and less verbose than existing alternatives. It also took a different approach when it came to React applications, it steered slightly away from what was very popular at the time the Elmish architecture (also known as MVU) and provided an API that is as close as possible to React itself.

Feliz introduced hooks which helped to simplify state management in some cases, as well as reduce the verbosity MVU can get when applications grow, since it was compatible with previous fable-react bindings it wasn't a big of an effort to migrate to Feliz.

Your typical Feliz UI component looks like this

[<ReactComponent>]
let Counter() =
    let (count, setCount) = React.useState(0)
    Html.div [
        Html.button [
            prop.style [ style.marginRight 5 ]
            prop.onClick (fun _ -> setCount(count + 1))
            prop.text "Increment"
        ]

        Html.button [
            prop.style [ style.marginLeft 5 ]
            prop.onClick (fun _ -> setCount(count - 1))
            prop.text "Decrement"
        ]

        Html.h1 count
    ]
Enter fullscreen mode Exit fullscreen mode

Its DSL is based on a list of properties for each kind of HTML tag, you can build reusable pieces of UI by just writing functions and other components given React's nature it is clear why Feliz is the most used, it simply fits with the F# mind, data and functions!

Feliz supports MVU via the Feliz.UseElmish package

type Msg =
    | Increment
    | Decrement

type State = { Count : int }

let init() = { Count = 0 }, Cmd.none

let update msg state =
    match msg with
    | Increment -> { state with Count = state.Count + 1 }, Cmd.none
    | Decrement -> { state with Count = state.Count - 1 }, Cmd.none

[<ReactComponent>]
let Counter() =
    let state, dispatch = React.useElmish(init, update, [| |])
    Html.div [
        Html.h1 state.Count
        Html.button [
            prop.text "Increment"
            prop.onClick (fun _ -> dispatch Increment)
        ]

        Html.button [
            prop.text "Decrement"
            prop.onClick (fun _ -> dispatch Decrement)
        ]
    ]

// somewhere else
ReactDOM.render(Counter(), document.getElementById "feliz-app")
Enter fullscreen mode Exit fullscreen mode

If you're looking to dive into Frontend F# then Feliz is a solid choice you will learn what most of the F# FE devs use and it is arguably the best choice today within the Fable realm.

The downsides of Feliz are the downsides of using React, since Feliz is a 1-1 binding over React you get the same problems React devs have, weird rules for hooks, easy to mistakenly provoke re-renders, and effects are still not entirely figured out in the react ecosystem and you have to keep manually what things need to re-render your UI. React uses Virtual DOM which was a performant way to render UI's in the early 2010's it is not the case anymore where browsers have caught up in performance and it turns out that in performance critical situations the VDOM diffing from React is just overhead rather than an advantage. For your average website, this shouldn't be a concern though but it is worth mentioning it.

Fable.Lit

This is my personal favorite one when it comes to Fable options, Fable.Lit builds on top of lit.dev which is a web component library built on web standards. It brings performant straightforward and inter-framework compatible components to the F# FE landscape since Lit works with DOM elements themselves rather than abstractions you can manipulate component instances like if you were doing vanilla JavaScript except that you can use the F# safety for that.

In Fable.Lit rather than building an F# DSL (we tried) we use a string-based alternative which is closed to the HTML you know and love, this also helps a lot when you have to consume web components like those from shoelace.style, fast.design, adobe spectrum components, and more, this will be a very important and big point over the next few years now that web components have taken off finally with major companies like Microsoft, Salesforce, Github, Adobe and more are using them.

Here's two ways you can use Fable.Lit Components

[<HookComponent>]
let functionCounter(initial: int) =
    let value, setValue = Hook.useState initial
    html
        $"""
        <!-- @<event name> means attach a handler to this event -->
        <sl-button outline variant="neutral" @click={fun _ -> setValue value + 1}>Increment</sl-button>
        <sl-button outline variant="neutral" @click={fun _ -> setValue value - 1}>Decrement</sl-button>
        <sl-badge>Count: {value}</sl-badge>
        """

[<LitElement("my-custom-element")>]
let MyCustomElement() =
    let host, props =
        LitElement.init(fun config ->
            config.props = {| initial = Prop.Of(0, attribute = "initial") |}
            // defaults to true if not set
            config.useShadowDom <- false
        )
    let value, setValue = Hook.useState props.initial.Value

    html
        $"""
        <sl-button outline variant="neutral" @click={fun _ -> setValue value + 1}>Increment</sl-button>
        <sl-button outline variant="neutral" @click={fun _ -> setValue value - 1}>Decrement</sl-button>
        <sl-badge>Count: {value}</sl-badge>
        """

// using both somewhere
html
    $"""
    Function Component:
    {functionCounter 20}
    <br>
    <!-- .initial means bind to "initial" property -->
    <my-custom-element .initial={10}></my-custom-element>
    """
Enter fullscreen mode Exit fullscreen mode

First of all if you are wondering "ugh strings", "that doesn't give any highlight", "the holes are not typed" I have a few words for that:

  1. Interpolated strings aren't as flexible as JS tagged templates so in .NET we fallback to using just objects and we lose type safety
  2. We actually have two extensions to give you the ability to highlight these F# strings

Here's the thing (and the main reason I like it):

  • Did we have to write bindings for sl-button?
  • Would we need to write bindings for any other custom element/web component?

The answer for both is No, we still have write bindings for the JS parts of the libraries we might use but when it comes to custom elements and other standard HTML elements we don't need to do anything, that includes it's attributes/properties.

The tradeoff is precisely that we gain access to a vast array of libraries out there but we lose some type safety when you describe your UIs.

And before I forget it, Fable.Lit also supports the Elmish architecture

type Msg =
    | Increment
    | Decrement

type State = { Count : int }

let init() = { Count = 0 }, Cmd.none

let update msg state =
    match msg with
    | Increment -> { state with Count = state.Count + 1 }, Cmd.none
    | Decrement -> { state with Count = state.Count - 1 }, Cmd.none

[<HookComponent>]
let Counter() =
    let model, dispatch = Hook.useElmish(init, update)
    html $"""
        <h1>{model.Count}</h1>
        <button @click={fun _ -> dispatch Increment}>Increment</button>
        <button @click={fun _ -> dispatch Decrement}>Decrement</button>
    """
Enter fullscreen mode Exit fullscreen mode

Lit itself is a pretty safe bet is a solid choice and built on web standards so it's very likely to have a really long life (it first came out around 2013-2014 as polymer if you ever head of that) and has adjusted and improved together with web browsers

Fable.Lit on the other hand is fairly new and the bindings may still have some areas where we can improve but the technology underneath is already production ready.

Sutil

When I first learned about Sutil I fell in love with it, it brought a lot of concepts from Svelte to the F# frontend landscape and while it's development has been slower than most it has some really interesting choices which can fit some minds better than the other alternatives.

Sutil is the first pure F# framework for the frontend that we have, it doesn't have bindings to any framework because it's just F#.

Sutil uses a Feliz variation of a DSL called Feliz.Engine so you get the F# type safety you know and love with reactive UI elements.
Sutil functions run only once and then everything is static unless you choose to be reactive via stores. This helps in regards performance and updates are only applied where things change.

In a similar fashion to Fable.Li, Sutil works with plain DOM elements which make it compatible with Web Components as well, it also provides features to write web components with it!

If you really like the programming model of svelte or rxjs (observables) then Sutil is something to look after, it also has built-in animations, chrome dev tools, and other nice features.

Here's how Sutil looks like

// functions can be separated from UI logic
// with some thought we can make these very reusable
// and even UI agnostic
let increment (counter: IStore<int>) =
    counter
    |> Store.modify (fun count -> count + 1)

let decrement (counter: IStore<int>) =
    counter
    |> Store.modify (fun count -> count - 1)

let view() =
    let counter = Store.make 0

    Html.div [
        // make this element reactive
        Bind.el(counter, fun count -> Html.p $"Counter: {count}")
        Html.div [
            // using stablished HTML elements
            Html.button [
                onClick (fun _ -> increment counter ) []
                text "Increment"
            ]
            // interoperation with custom elements
            Html.custom("sl-button", [
                Attr.custom("variation", "neutral")
                onClick (fun _ -> decrement counter) []
                text "Decrement"
            ])
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

As mentioned before whenever we mount/call view it will render once and when stores/observables emit a new value only the reactive parts will update this allows for fine grained updates

Sutil Also offers MVU support:

type Msg =
    | Increment
    | Decrement

type State = { Count : int }

let init() = { Count = 0 }, Cmd.none

let update msg state =
    match msg with
    | Increment -> { state with Count = state.Count + 1 }, Cmd.none
    | Decrement -> { state with Count = state.Count - 1 }, Cmd.none

let Counter() =
    let model, dispatch = () |> Store.makeElmishSimple init update ignore
    Html.div [
        disposeOnUnmount [ model ]
        Bind.fragment (model |> Store.map getCounter) <| fun n ->
            Html.h1 [ text $"Counter = {n}" ]

        Html.div [
            Html.button [
                onClick (fun _ -> dispatch Decrement) []
                text "-"
            ]

            Html.button [
                onClick (fun _ -> dispatch Increment) []
                text "+"
            ]
        ]]
Enter fullscreen mode Exit fullscreen mode

Part of the disadvantages in Sutil is the slow updates although David recently mentioned that he will be working on it, more maintainers would be welcome.
Also it has been in beta for a while so it might not be so ready for prime time.
More testing from real users would be nice because at least on my relatively limited testing it feels just as solid as any of the previous choices.

Feliz.Engine

Once the high profile projects have been mentioned one project that is worth mentioning and one you can use if you plan to ever bring another framework to the F# space is Feliz.Engine

Feliz.Engine is a library that defines in a standard way F# DSLs for Elements, Attributes and Styles. It was born out of the Original Feliz DSL but modified slightly to fit a more general use case.

Sutil, Feliz.Solid and Feliz.Snabdom use Feliz.Engine under the hood you could also use it to bring others to the fold!

This project deserves a mention just for it's potential to bring more to the ecosystem (and to not confuse it with Feliz itself)

Fable.Svelte

These bindings were a way to make F# work with .svelte files. I don't have much to say about this other than it exists and you could take a look if you want but it's usage is fairly low.

For this one I don't think is a good choice for your next serious project, maybe for experiments here and there but given it's low usage there might be some bugs not found just yet in the way, Sutil would be a better choice if you like Svelte-like way of doing UIs

Svelte of course is rock solid as a choice and is one of the most popular JS frameworks out there, the problem lies on how mature are the bindings and how battle tested they are

Feliz.Snabdom

Using Feliz.Engine comes Feliz.Snabdom as well snabdom has a virtual dom implementation but deals with DOM elements rather than abstract over them (like react) this gives you more wiggle room to interoperate with third party components. it provides life cycle hooks, lazy loaded elements and other features.

I'm not fan of virtual dom myself so I haven't really tried this much more than just a few examples at the same time I'm not sure of the maturity of the bindings, although snabdom has been out for years and has been used by thousands of devs the concerns lie in how portable the code is + how mature the bindings could be.

Fable Next!

These new options are coming hot from the oven and paint a bright future for Fable integrations on the Frontend ecosystem!

Fable 4 (snake island) is going to bring JSX compilation, meaning that frameworks that use JSX as their DSL and building blocks will have an even easier time integrating, this support will come to Feliz.Engine as well this means a couple of things

  • Stable API for the F# side (Feliz.Engine)
  • Broad Target of UI frameworks by just configuring the packages you want to use (be it solidjs, vue jsx, inferno, preact etc)
  • Easier migration paths between F# <-> JS

Given how JSX is still a compilation step you can always fallback to manual JSX and continue from/to JSX if needed.

Feliz.Solid

This is an exciting one, [solid.js] has been going up in popularity these days because it is what react could have been

  • True and Predictable reactivity
  • No manual dependency tracking
  • Observable support
  • No Virtual DOM
  • Fast and Efficient
  • Small footprint Library

So if the react model is your thing and you want to avoid many of the react footguns then this is something to keep an eye for

Solid code looks like this:

[<JSX.Component>]
let Counter() =
    let count, setCount = Solid.createSignal(0)
    let doubled() = count() * 2
    let quadrupled() = doubled() * 2

    Html.fragment [
        Html.p $"{count()} * 2 = {doubled()}"
        Html.p $"{doubled()} * 2 = {quadrupled()}"
        Html.br []
        Html.button [
            Attr.className "button"
            Ev.onClick(fun _ -> count() + 1 |> setCount)
            Html.children [
                Html.text $"Click me!"
            ]
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

As you can see it is very similar to Sutil or Feliz.Snabdom and that's because it is using Feliz.Engine as well! while these don't interoperate between themselves easy because each library defines what the DSL actually emits: DOM Elements, Virtual DOM elements they do use the same DSL so learning one it basically teaches you the others as well!

The main disadvantage of this is that is of course new, it only works on Fable 4 (Alpha at the time of writing) and thus should not be considered for your next serious project. Once Fable 4 is released for real then it could be something to really consider and contribute to.

Fable + JSX

If you're thinking "my favorite framework is not in the list" do not worry, writing bindings for Fable has become easier over the years, specially when you take Feliz.Engine into account Fable 4 will bring JSX as well meaning that it could be even simpler to integrate to the JavaScript ecosystem.

Perhaps you like vue, but supporting vue files is too much, perhaps the framework you are using at work supports JSX this has the potential to bring a lot with minimal changes, like Alfonso said

It's like programming against an interface (JSX) instead of an implementation (the compiled JS)

Although he also said there are nuances and likely differences in how each framework and its tooling treats JSX so Feliz.Engine is close to universal but not that universal.

WebSharper

Web sharper has been out for quite some time and has an interesting F# first approach to the UI, WebSharper aims to fulfill the full-stack F# promise hiding away some of the JS details but has some pretty good capabilities when it needs to interoperate with javascript.

Rather than a set of multiple libraries and frameworks WebSharper is a one stop shop all style of framework

A simple Web Sharper Application looks like this:

[<Website>]
let Main =
    Application.SinglePage (fun ctx ->
        Content.Page(
            h1 [] [ text "Hello World!"]
        )
    )
Enter fullscreen mode Exit fullscreen mode

This will tell WebSharper to generate some JavaScript and run it directly on the body of your application.

While WebSharper has a ViewModel strategy it also offers MVU support, for example a counter can look like this

[<JavaScript>]
module Counter =

    type Model = { Counter : int }

    type Message = Increment | Decrement

    let Update (msg: Message) (model: Model) =
        match msg with
        | Increment -> { model with Counter = model.Counter + 1 }
        | Decrement -> { model with Counter = model.Counter - 1 }

    let Render (dispatch: Dispatch<Message>) (model: View<Model>) =
        div [] [
            button [on.click (fun _ _ -> dispatch Decrement)] [text "-"]
            span [] [text (sprintf " %i " model.V.Counter)]
            button [on.click (fun _ _ -> dispatch Increment)] [text "+"]
        ]

    let Main =
        App.CreateSimple { Counter = 0 } Update Render
        |> App.Run
        |> Doc.RunById "main"
Enter fullscreen mode Exit fullscreen mode

or if you prefer HTML

<!-- this is inside the HTML page you're serving -->
<body>
  <button ws-onclick="OnDecrement">-</button>
  <div>${Counter}</div>
  <button ws-onclick="OnIncrement">+</button>
  <script type="text/javascript" src="Content/Counter.min.js"></script>
  <!--[BODY]-->
</body>
Enter fullscreen mode Exit fullscreen mode

[<JavaScript>]
module Client =
    // The templates are loaded from the DOM, so you just can edit index.html
    // and refresh your browser, no need to recompile unless you add or remove holes.
    type MySPA = Template<Snippet.IndexHtml, ClientLoad.FromDocument>

    type Model = int

    type Message =
        | Increment
        | Decrement

    let update msg model =
        match msg with
        | Message.Increment -> model + 1
        | Message.Decrement -> model - 1

    let view =
        let vmodel = Var.Create 0

        let handle msg =
            let model = update msg vmodel.Value

            vmodel.Value <- model

        MySPA()
            .OnIncrement(fun _ -> handle Message.Increment)
            .OnDecrement(fun _ -> handle Message.Decrement)
            .Counter(V(string vmodel.V))
            .Bind()

        fun model ->
            vmodel.Value <- model

    let Main =
        view init

Enter fullscreen mode Exit fullscreen mode

There's a whole website with demos you can try!

WebSharper also offers a reactive model that can be used in exchange of the MVU architecture, the last example could also be simplified to the next example:

[<JavaScript>]
module Client =
    // The templates are loaded from the DOM, so you just can edit index.html
    // and refresh your browser, no need to recompile unless you add or remove holes.
    type MySPA = Template<Snippet.IndexHtml, ClientLoad.FromDocument>
    let counter = Var.Create 0
    let Main =
        MySPA()
            .OnIncrement(fun _ -> counter.Value <- counter.Value + 1)
            .OnDecrement(fun _ -> counter.Value <- counter.Value - 1)
            .Counter(V(string counter.V))
            .Bind()
Enter fullscreen mode Exit fullscreen mode

This reactive style is similar to the new Vue's Composition API so regardless of your choices WebSharper has you covered.

That being said, on my twitter bubble WebSharper is not one of the most popular ones and I'm not entirely aware why my guess is that it tries to hide JavaScript as much as possible to try to stay in F# and that could create some sort of vendor lock in and friction in some cases, this doesn't mean it is a bad choice though if you don't want to build on top of the JavaScript ecosystem that much it looks like solid technology to pick up specially because it offers paid support so this can be a good fit for teams rather than individuals.

If you want to learn more about WebSharper let me know, so I can explore more and dedicate a couple of blog posts to it.

Web Assembly

In this Section:

  • Bolero
  • Fun.Blazor
  • Avalonia.FuncUI

Web Assembly is the newest player in the game and one that is being/will be a true game changer with it comes to web development, just as a taste of it's power you can now use photoshop natively in the browser now, what does that mean for you as a .NET developer?

It means that you can run F# code (or C# if that's your jam) natively on the browser, no intermediary JavaScript that you have to touch and you keep the safety of F#

Why Web Assembly and who benefits most from it?

Web assembly (WASM) is meant for users who want to run native code in the browser, this has a few implications, WebAssembly does not have so far access to DOM and neither a garbage collector, so for WASM apps to work with .NET you need to load a .NET runtime + your application's code. That means that any time your website is visited you have to wait a few seconds while your web app loads the runtime + your code.
Any time that any of these technologies have to they need to share information with the JS world and this can be costly, while you as an App developer don't have to do it manually you still need to be wary of the costs of serialization/deserialization that are made any time you share information with the JS world, be it in form of big UI trees, large amounts/rows of data and similar situations.

That being said, if you can afford these drawbacks then you will be able to enjoy F# safety in all of its glory, no more weird JavaScript emits, or trying to bind an interface to a JavaScript object and hopefully that holds true at runtime. It's just the real deal.

You can leverage the .NET ecosystem when it comes to libraries with all of their patterns and knowledge you already may have. This also means that since you are using .NET you can share logic and data 100% with the server, after all .NET6 libraries (unless they are using OS specific APIs) work on ASP.NET core for the server and WASM, this means no shared folders with tweaked paths and #if FABLE or similar directives it's just the assembly being shared as is.

While you don't need to interact with JavaScript at all, you can do so if you must there are ways to interoperate with functions declared in the global scope or even in JavaScript modules.

So you're not entirely isolated you can interact with the outside world if needed.

DISCLAIMER: Most of these alternatives rely on Blazor which is the product solution offered by Microsoft to tap into WebAssembly with C# as usual F# is not in the roadmap but the community always jumps in to save the day and offer F# devs the experiences they deserve.

Bolero

Bolero is the most stable and mature solution for F# web assembly, it is brought to you by the same devs behind web sharper, in a way it could be the next step in their path to make web applications with F#. Bolero offers most of the functionalities of web sharper but native this time, HTML Templates via a Type Provider, Client Routing with Discriminated unions, F# DSL, MVU.

Your typical Bolero view looks like this


type Model = { value: int }
let initModel = { value = 0 }

type Message = Increment | Decrement
let update message model =
    match message with
    | Increment -> { model with value = model.value + 1 }
    | Decrement -> { model with value = model.value - 1 }

let view model dispatch =
    div {
        button { on.click (fun _ -> dispatch Decrement); "-" }
        string model.value
        button { on.click (fun _ -> dispatch Increment); "+" }
    }

let program =
    Program.mkSimple (fun _ -> initModel) update view

type MyApp() =
    inherit ProgramComponent<Model, Message>()

    override this.Program = program

Enter fullscreen mode Exit fullscreen mode

It is pretty similar to the other MVU samples we've seen so far bolero also offers interoperation with Blazor concepts like external components from third party libraries, remoting (RPC client-server style of communication) and of course Pure F#.

If you liked the approach that WebSharper gave you and you're looking for the next step then bolero will be just what you want you can translate concepts and knowledge from there. The sad part for me is that it doesn't have a reactive style when it comes to handling state MVU is great but on larger applications it just doesn't cut it for me, the nice thing of the bad is that you can create multiple elmish components in your application and use parameters so you don't have a single master Elmish update function and rather each component have it's own state.

Fun.Blazor

This is another abstraction built on top of Blazor, it is the new kid in town and has some very enticing models when it comes to handle state. Fun.Blazor recently put out its version 2.0.0 which adds a bunch of nice things all around, it allows you to create UI's with F# Computation Expressions (CE) in a similar fashion to bolero, it also has the ability to use string templates (much like Fable.Lit) and has some options for routing like a giraffe style router it offers seamless integration with blazor's dependency injection, very useful to interoperate with JavaScript and other Blazor services.

Your typical Fun.Blazor component looks like this:

adaptiview() {
    let count, setCount = cval 0 // changeable value

    h1 { $"Counter: {count}"}

    button {
        onclick (fun _ -> setCount count + 1)
        "Increment"
    }

    button {
        onclick (fun _ -> setCount count - 1)
        "Decrement"
    }
}
Enter fullscreen mode Exit fullscreen mode

Fun.Blazor brings the power of FSharp.Data.Adaptive to allow incremental updates to the view, this package works like excel cells, where one cell may be the source of truth for others, the other cells can recompute values depending on the first cell this allows for performant updates in the UI because only the reactive parts change. This model is very close to Sutil's model as well In fact you can use stores in Fun.Blazor as well

let myComponent() =
    html.comp(fun (hook: IComponentHook) ->
        let counter = hook.UseStore 0
        let double =
            store.Observable
            |> Observable.map(fun n -> n * n)
            |> AVal.ofObservable counter.Current hook.AddDispose

        adaptiview() {
            let! countValue, setCount = counter.WithSetter()
            let! doubleValue = double

            h1 { $"Counter: {countValue}, Double: {doubleValue}"}

            button {
                onclick (fun _ -> setCount countValue + 1)
                "Increment"
            }

            button {
                onclick (fun _ -> setCount countValue - 1)
                "Decrement"
            }
        }
    )
Enter fullscreen mode Exit fullscreen mode

Adaptive views are a concept that I'd love if other frameworks implemented because the reactive model really resonates with me that being said, I know you want to see elmish in action, so I'm pleased to tell you that yes, it supports MVU as well

type Model = { value: int }
let initModel = { value = 0 }

type Message = Increment | Decrement
let update message model =
    match message with
    | Increment -> { model with value = model.value + 1 }
    | Decrement -> { model with value = model.value - 1 }
// using elmish directly
html.elmish (init, update, fun state dispatch ->
    div {
        h1 { $"Count: {state.value}" }
        button {
            onclick (fun _ -> dispatch Increment)
            "Increment"
        }
        button {
            onclick (fun _ -> dispatch Decrement)
            "Decrement"
        }
    }
)

// using elmish with adaptive views
html.comp (fun (hook: IComponentHook) ->
    let state, dispatch = hook.UseElmish(init, update)
    div {
        adaptiview() {
            let! count = state
            h1 { $"Count: {count.value}" }
        }
        button {
            onclick (fun _ -> dispatch Increment)
            "Increment"
        }
        button {
            onclick (fun _ -> dispatch Decrement)
            "Decrement"
        }
    }
)
Enter fullscreen mode Exit fullscreen mode

Fun.Blazor has a lot of potential it is still young it needs more real world usage to validate many of the efforts taken in v2.0.0, although young it feels like a solid option, but keep in mind that just like Sutil it has only one maintainer, so if you like this you should look into contributing to the framework because it feels like really good option.

Avalonia.FuncUI

This one might come as a surprise for many because Avalonia is a Desktop application framework! but as seen in this Avalonia.FuncUI WASM Template it is possible to bring the power of avalonia into the desktop via WASM, The main advantage of Avalonia.FuncUI is that you will be able to share code between browsers, android, ios, mac, linux, and windows.

Avalonia.FuncUI was also recently updated and added this reactive-like model in v0.5.0

Component(fun ctx ->
    let state = ctx.useState 0
    DockPanel.create [
        DockPanel.verticalAlignment VerticalAlignment.Center
        DockPanel.horizontalAlignment HorizontalAlignment.Center
        DockPanel.children [
            TextBlock.create [
                TextBlock.dock Dock.Top
                TextBlock.text (string state.Current)
            ]
            Button.create [
                Button.dock Dock.Bottom
                Button.onClick (fun _ -> state.Current - 1 |> state.Set)
                Button.content "-"
            ]
            Button.create [
                Button.dock Dock.Bottom
                Button.content "+"
                Button.onClick (fun _ -> state.Current + 1 |> state.Set)
            ]
        ]
    ]
)
Enter fullscreen mode Exit fullscreen mode

The concepts of IWritable<'T> and IReadable<'T> work just like adaptive/changeable/stores/observables that we've seen before in sutil/fun.blazor so Avalonia.FuncUI could start becoming a competitor in the Web landscape specially if you already have some desktop app development experience, this is part of the power of WASM in practice in the case of Avalonia.FuncUI WASM you don't really need to know anything of web development to get started, just jump in!

That being said, Avalonia uses Skia to render in a canvas (likely using webgl) so you won't have any kind of DOM nodes to inspect and also as far as I'm aware (and I'd love to be corrected if necessary) you throw accesibility out of the window because of that so assistive technology won't work with this kind of web sites, it also worth noting that as far as I'm aware (at the time of writing) Web support in Avalonia is in beta state so there might be a couple few bugs lurking out there.

MAUI

The elephant in the room here would likely be MAUI because it also has blazor support but I'm going to be very dismissive here and I plead guilty about it because it is a Microsoft product, I'd prefer to Microsoft to step out of the way of its .NET ecosystem and I'd love to better cultivate alternatives like Uno/Avalonia, but whatever right?

  • Good Luck about F# support
  • Good Luck to have linux support

Both things can be done with Avalonia without any major issues, also all of the other alternatives discussed here can be developed on any of the three major operating systems heck even from your Raspberry Pi 4 that's not something I'd expect in MAUI soon.

Takeaways

That being said the F# Frontend Landscape is not that big it might feel confusing with all the tweets and news going all over the place but thankfully as most things F#: We are pretty much settled how we use things and even if there is variety most alternatives can live together in one way or another.

Here's the tl;dr:

Use Fable if:

  • You want to take advantage of the JavaScript Ecosystem
  • You want to have the possibility to migrate away from F# if necessary
  • You want the lowest amount of kb of resources to the browser.
  • You must interact with JavaScript on a heavy basis
  • You Like and want to use React or Lit or Vanilla (Sutil)

Don't use Fable if:

  • You really dislike JS
  • You want true type safety
  • You can afford go vanilla F# (i.e. not use most of the JS ecosystem)
  • You don't want to learn or deal with the JavaScript Tooling

Use WebSharper if:

  • You don't mind about going F# first JS second
  • You want type safe HTML via type providers
  • You want to blur the lines between client side F# and server side F#
  • You don't care too much of the toolchain and only care about final deployable assets

Don't use WebSharper if:

  • You need to literally make manual js files and adjustments to the compiling toolchain
  • You need a more JavaScript oriented application
  • You need to build on top of the existing node tooling
  • You care about an extra runtime for your application (around 8kb)

Use Web Assembly if:

  • You want to run native F# in the browser not a fake one
  • You want to leverage the .NET ecosystem
  • You want to enjoy .NET tooling to publish, distribute and build your F#
  • You want to share code between Desktop, Server, and Mobile (like Avalonia will allow you)

Don't use Web Assembly if:

  • You don't want to ship heavy websites (even if there's trimming support for some libs)
  • You need low TTI (time to interaction) and TFP (Time to Fist Painting)
  • You need a more mature ecosystem
  • You need heavy JS interop

Personal Opinion

No one asked for it, and no one should because my opinion shouldn't have any weight on your decision making.
That being said...

My personal top is:

  1. Fable.Lit
  2. Sutil - Fun.Blazor
  3. Feliz.Solid?

The main reason is that Fable.Lit checks the boxes when it comes to web standards and I'm primarily a frontend developer and React is just not my thing mainly due to it's focus on hooks which in the case of React they might make sense but are too magical and very prone to errors and performance issues if badly used (I'm looking at you useFootGun, I mean useEffect)

Sutil comes in second because of it is extremely awesome to have a pure F# framework that also offers a reactive state management model it simply fits my mental model of doing websites.

Fun.Blazor comes swiftly on second place as well for the Tie because it takes the same concepts sutil uses for state management and takes it a step further

Feliz.Solid comes in third because it also offers a reactive model that is likely going to replace React in a lot of places and codebases in the future. It doesn't suffer of the same problems React has and its future looks Bright, its author was recently hired (at the time of writing) at Netlify so it can only go up from here while the Feliz integration matures.

Conclusion

So there you have it I hope this post sheds some light on what the current state of F# frontend is and what you should take into account if you want to choose one or the other alternatives we have.

Ultimately you should not be pressured to chose right. F# solutions even if they are not "mature" are pretty solid, after all that's the main reason we chose F# to either work with or have fun (sometimes both) you should avoid one of these alternatives if you truly have reasons not to pick it otherwise it's likely going to work your needs.

These frameworks are excellent work of members of the F# community even if they look young or are in beta these tools are extremely well done and are far more capable than what you would think when you hear those words give them a try give feedback to its authors and remember not everything is React or derivatives you have choices today :)

Until the next one, don't forget to leave your comments and questions if needed!

Discussion (19)

Collapse
kspeakman profile image
Kasey Speakman • Edited on

Great article and nice overview!

One omission. You can use "plain" Elmish, with Fable translating to JS, and React for DOM rendering. I only use React as an HTML DSL. So view code maps 1:1 with the rendered HTML, but with the power of F# (conditionals, loops, etc). And no extra concepts like hooks, stores, components, etc. Though not without its faults, I find it preferable and much simpler than the options presented here.

I wish there were a HTML DSL on top of a Svelte-like (memoization) renderer. It would be a great alternative to using React rendering. As it is, I think Svelte (and Sutil) rendering is tightly coupled with its bespoke component-based pattern.

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author • Edited on

Thank you for making the time to read the posting I'm very thankful!
The main reason I do not include Elmish by itself is because every option presented here supports it either alone or together with better and simpler state management solutions.

And because I do not think Elmish alone is actually good for most applications, a couple of counters here and there is good but when you have to handle multiple pages or isolate state in one of your pages it becomes a burden rather than an advantage.

I've written my fair share of Elmish apps myself and I always have favored other options because they are more flexible, or in combination with Elmish they become more powerful

I only use React as an HTML DSL. So view code maps 1:1 with the rendered HTML, but with the power of F# (conditionals, loops, etc). And no extra concepts like hooks, stores, components, etc.

Every option listed here does the exact same thing with or without React and if required all of them can be used just Elmish rather than the other state management solutions

I wish there were a HTML DSL on top of a Svelte-like (memoization) renderer. It would be a great alternative to using React rendering. As it is, I think Svelte (and Sutil) rendering is tightly coupled with its bespoke component-based pattern.

That's Sutil for you, Sutil does not implement any kind of component based style, Sutil is just functions and DOM elements the same can be said for Fable.Lit but that's not an F# based one

Collapse
kspeakman profile image
Kasey Speakman • Edited on

No problem. I am happy to contribute something to the article.

every option presented here supports it either alone or together with better and simpler state management solutions

All the examples use components (objects) in the large and Elmish in the small. This definition of "supporting" Elmish (aka MVU) is quite different from the functional-style UI option I mentioned. Hopefully you can see why I considered that (no functional style UI in a functional-first language) an omission.

And because I do not think Elmish alone is actually good for most applications, a couple of counters here and there is good but when you have to handle multiple pages or isolate state in one of your pages it becomes a burden rather than an advantage.

My experience is the opposite. I used various component frameworks for many years. They were easy to get started, but always became exponentially more difficult to maintain as the apps got larger. And for mostly technical reasons around object / framework conformance. Then I found MVU, which has the opposite problem -- high initial overhead (esp. wiring), but ever smaller increases to overhead as the project grows.

We've used MVU for the last 6 years, and for more than a couple of counters. Of the 10ish UI apps, the largest F# one has ~50k loc and 125 pages, many are page fragments, various depths. The wiring overhead has advantages too: code is explicit, traceable, functional, and free of framework-proprietary concepts. That's not to say MVU is perfect or even fully baked. (Is anything?) And I know too well that it's a hard transition from years of practice with components. But it is a very effective option, and the only one I've seen that is capable of using deterministic functions all the way down. I'm assuming benefits of those don't need to be enumerated to an F#/FP audience.

Sutil does not implement any kind of component based style, Sutil is just functions and DOM elements

So I don't have to use binds, stores, lifecycle events, or any componentization things? Aka, I can use "pure components", all module (static) functions, no object constructor, and it still does memoized rendering? I hadn't seen any examples of that. Based on the examples, the memoization seemed inseparable from its component-oriented design.

The other issue with Sutil for me is Feliz. Perhaps I am cynical from years of component frameworks that hide things which bite me later. But I prefer to code against the web tech. Structurally, Feliz is not quite that. And it mixes in separate concerns like hashed CSS, animation. I'd prefer to compose different concerns with library approach. And if custom coded, represented like it will appear/operate in the web tech. This keeps my code tied to long-lived/backward-compatible web standards rather than bespoke, transient framework abstractions.

Thread Thread
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

Hopefully you can see why I considered that (no functional style UI in a functional-first language) an omission.

I see why some people like to go full functional and I think I understand what you mean with that.

I'm glad that MVU is working for you and your company on that scale I've always found it hard to grasp when things grow, specially because once you grow a certain amount you have to start abstracting types and function signatures for it to keep it readable (continues on the next section)

The wiring overhead has advantages too: code is explicit, traceable, functional, and free of framework-proprietary concepts.

This generates IMO some indirection that I can't really handle cognitively and many others I've known who have left elmish like abstractions behind like elmish itself, redux and similar patterns.
Even the react ecosystem itself was primarily using Redux but the industry moved on because not everyone was able to work on these architectures alone it still has it's use cases but it is not in vain that different alternatives surfaced to avoid going full elm style, granted that I'm biased against a pure functional approach as well so those things could be tied to that.

But it is a very effective option, and the only one I've seen that is capable of using deterministic functions all the way down. I'm assuming benefits of those don't need to be enumerated to an F#/FP audience.

I agree it is the only way to truly go functional

So I don't have to use binds, stores, lifecycle events, or any componentization things? Aka, I can use "pure components", all module (static) functions, no object constructor, and it still does memoized rendering?

One thing that looks like is kind of an omission here is that react is a rendering library that works with components even if you use it in a static way you are using components underneath that's how every element is working so I don't think using Feliz, fable-react, lit or any other F# alternative will actually get you away from components.

back to the question, as it is stated on your question No, you still have to use a store, this store is backed by your elmish model though you can see a full example here . The reason you need to bind fragments here is that rather than memoizing Sutil functions only run once (so there's not even need for memoizing) and only those fragments will change. This introduces you to maybe 1-5 framework concepts so this breaks what you are looking for I guess. In any case underneath of it (just like react) it will do what it needs to print everything on screen while it offers component-like features (just like react) you don't need to use them.

The other issue with Sutil for me is Feliz. Perhaps I am cynical from years of component frameworks that hide things which bite me later. But I prefer to code against the web tech. Structurally, Feliz is not quite that. And it mixes in separate concerns like hashed CSS, animation.

There's a miss understanding here, Sutil uses Feliz.Engine as explained in the article it is a library that abstracts HTML representations into the types that a rendering library may need to construct Virtual DOM elements or DOM elements, Feliz.Engine doesn't do anything related to hashing css or "scoped css" or anything like that the CSS part of the engine is abstracting types that can be used to represent standard CSS it's up to the library user to decide what will be done with that css it can be used to produce stylesheets or even inline css strings but that's is the library's consumer concern

Perhaps you might mean the original dialect Feliz (the library not the engine) which rather than HTML you're right is not entirely that is just an abstraction over React dialect of html

I'd prefer to compose different concerns with library approach. And if custom coded, represented like it will appear/operate in the web tech.

That's what Sutil did with Feliz.Engine it used the library to compose raw DOM elements that can be mounted on any HTML element you can't get closer to web standards than that (using raw DOM elements)

This keeps my code tied to long-lived/backward-compatible web standards rather than bespoke, transient framework abstractions.

Agreed that's what both Sutil, Fable.Lit build on top just HTML and web standards, arguably Solid could fit into this category as well but granted that one has more things going on.

I think this also brings other things to the table, I think the main reason elmish gets relegated to lesser usages than the mainstream is that you can accomplish similar things with less verbosity and I agree that you lose the benefits of traceability and explicitness that elmish provides but the end result and time invested is what often matters

Now I will reiterate that all of these support Elmish and you can go just as full elmish as elmish + fable-react (which I think is the version you're speaking of) with a couple of extra concepts or a mix between majority elmish minority components or majority components minority elmish or just plain components or plain elmish

I'd think the best way to see how do they fit on your scale is to try their elmish support (if you are ever looking for an alternative which sounds like you're fairly comfortable with your setup and that's great no need to change) and give some feedback on what you think they're lacking to have Full Elmish support

Thread Thread
kspeakman profile image
Kasey Speakman • Edited on

This generates IMO some indirection that I can't really handle cognitively and many others I've known who have left elmish like abstractions behind like elmish itself, redux and similar patterns.

I think I understand what you mean. Early Elm patterns had a child message pattern that was very tedious and hard to resolve some needs. Elmish's implementation of the Cmd and subscription signatures, though they are quite logical and flexible, always feel like a mental obstacle course to work thru. As I alluded to earlier, I don't think MVU is quite complete as yet. It needs more investment. I had to augment it with at least 1 new core concept and some add-on patterns to make it work for us.

Even the react ecosystem itself was primarily using Redux but the industry moved on because not everyone was able to work on these architectures alone it still has it's use cases but it is not in vain that different alternatives surfaced to avoid going full elm style, granted that I'm biased against a pure functional approach as well so those things could be tied to that.

There are some other factors. Redux author became a public face of the React team. Redux has a similar incompleteness problem to MVU. But I think the core issue here is that it is VERY hard to change out of an object-based mindset. At least in my experience. It took years / multiple attempts to chip away at my deeply ingrained OO-shaped instinct on the how to code. I thought I was there. Then after my first significant Elm project, I realized I still hadn't got it. That's when I finally started to understand functional patterns. So I know first hand that sometimes I just can't break my brain anymore and I go back to what I know. Conversely, the 3 devs I've trained with no previous programming experience were usefully contributing MVU UI code after about a month.

One thing that looks like is kind of an omission here is that react is a rendering library that works with components even if you use it in a static way you are using components underneath that's how every element is working so I don't think using Feliz, fable-react, lit or any other F# alternative will actually get you away from components.

Not an omission. It uses components when you take a complete xray. But the view code that I write is still deterministic and structurally similar to the rendered DOM. Except for event handlers, which are minor holes in both, but still look like an HTML syntax. Since we only use React in this way, the team only has to know HTML, no further knowledge dependency to unlearn when the next better thing comes out.

No, you still have to use a store, this store is backed by your elmish model though you can see a full example here .

Yes, this is a side effect when my goal is deterministic code for logic and view generation. Loading a store is not much different conceptually from passing it as a function argument. I prefer the latter because there's no hidden magic. I've been bitten by magic approaches many many times. They're never quite magic enough to cover all things I run into. Then I have to work around or against it to accomplish what I need. That kind of overhead is very costly in the long term.

There's a miss understanding here, Sutil uses Feliz.Engine ... Perhaps you might mean the original dialect Feliz

Yes, the last Feliz I researched was the library. (Regarding the scoped css, animation.) The Engine version syntax is a further step removed from the rendered HTML. It's less cognitive load when it matches more closely.

// Fable.Elmish.React
div [Class "foo"] [
    str "bar"
]
Enter fullscreen mode Exit fullscreen mode
<!-- rendered -->
<div class="foo">
    bar
</div>
Enter fullscreen mode Exit fullscreen mode

Now I will reiterate that all of these support Elmish and you can go just as full elmish as elmish + fable-react (which I think is the version you're speaking of) with a couple of extra concepts or a mix between majority elmish minority components or majority components minority elmish or just plain components or plain elmish

I don't agree here. A couple of extra concepts and mixing in components, if it requires you to use its code abstractions, is a framework. "Full" MVU is not that. (I'm not at all minimizing MVU in the small. It is a great tactic.) MVU is a library approach where I opt into library abstractions. So I can choose a different renderer for example, without changing my page logic. (There used to be an alternative renderer for Elmish.) With MVU the functions and types I use are only the ones I defined. (I don't use Elmish's Cmd. Dispatch is in terms of my Msg type. And the Elmish-specific stuff is only called once at app startup.) The extra knowledge dependency of MVU is the pattern, which is very different from depending on framework abstractions in each page. Invariably a framework author learns better ways to do things. And abstractions change. I've been in situations where it was simply not feasible to upgrade. Then all ecosystem efforts go to the new version, and the app stagnates, and the team wants to rewrite it. This is perhaps a cost we don't consider when choosing a component-oriented (aka object aka framework) approach. (Actually there are more costs, like systematically training devs to prematurely abstract, which creates a large maintenance cost long-term.) I guess we assume it's the cost of doing business. But I can avoid a huge chunk of that with a functional architecture.

I'd think the best way to see how do they fit on your scale is to try their elmish support (if you are ever looking for an alternative which sounds like you're fairly comfortable with your setup and that's great no need to change) and give some feedback on what you think they're lacking to have Full Elmish support

Hopefully the above explains what is needed to support functional architectures like MVU. (No required usage of objects, no systemic code abstractions that user code needs to depend on, nor required side effects in logic / view calculation. Basically, recipes composing single-purpose libraries rather than frameworks are needed to support functional architecture.) But I don't think popular projects will be looking at this as a primary use case, because most of us devs are not interested. We just want a familiar framework approach that "gets it right this time". It's an apt description of me for nearly 2 decades.

Collapse
kaashyapan profile image
kaashyapan

What is your go to for client SPA routing ?

For my last pet project I went with github.com/fable-compiler/Fable.Store with svelte and routify for routing.
I wish you had mentioned Fable.Store.

For my next if I can manage to avoid js deps and the fear of needing js deps in the future, I might go full blazor. I love the router infer.
When Blazor was still young, I remember splitting stuff into mini MVU 'components' and passing messages between components via AsyncRx. It was a scalable model.

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

What is your go to for client SPA routing ?

I would normally use whatever the framework brings (Feliz.Router in case of Feliz, Fun.Blazor has its own, bolero has it's own) and if there's not a specific library in the framework I write some custom bindings over navigo on the projects that I need it because it is fairly small and does what I need most of the time.

For personal projects I use a more simple approach, I use DU's and pattern matching to just show what I need to show as if it was like a router

I wish you had mentioned Fable.Store.

I did mention Fable Store although, with a different name (Fable.Svelte)
I haven't seen much movement around it otherwise I would for sure talked more about it it would be nice if you could also write about how you're using it and why did you chose it!

Collapse
merthod profile image
Merthod

Hi, I'm from planet JS/TS. Can you tell me in a nutshell why you do like F# for front-end so much? Or can recommend some resource?

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

Hello @merthod thanks for asking

The primary reason I like F# in general is that is a language that has helped me be a better programmer, it has improved the way I see and work with code, taught me how to program in a safer way (i.e. prevent null/undefined from even being part of the workflow), how to write smaller and more meaningful functions, how and when to use mutability and when to hide it so consumers can still program in a safe way. Overall it is a fantastic language that helps me and my brain to write better, safer and simpler programs with less effort and resulting bugs.

In the case for Frontend development, it is due to the reasons above and for the same reason some people like to go full-stack with JavaScript using a language you're proficient and that has type safety waranties as F# is just a very good option.

If you like typescript because it helps you write safer code and avoid many javascript's dynamic nature pitfalls and similar features, then F# is a very good language to check out

if you want to have a check on the language itself I'd suggest to read my other articles on F# specially this series, there's also this wonderful book zaid-ajaj.github.io/the-elmish-boo... which can be of use to learn more although it might be a little bit outdated in some areas but regardless it is a very good resource

on twitter there's a whole community aroun the #fsharp hashtag, there's an official F# slack you can check fsharp.org/guides/slack/ and also we have an unofficial discord chat discord.gg/R6n7c54 where you can ask questions about the language and someone will try to give you some answers

Collapse
merthod profile image
Merthod • Edited on

Thanks for the thoughtful reply. Was it long util you got good in it?

Thread Thread
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

I can't say an exact number for sure, it took a while because I had a really hardwired OOP (Java style) thought process but since I was just doing projects and writing code here and there for fun one day I was already doing bigger project.

However within 3 months I already felt quite productive with F# even if I wasn't good at it so there's that

Collapse
frodolight profile image
Frodo

Very valuable article

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

Thank you! I appreciate the kind words!

Collapse
ambriel profile image
Ambriel Goshi

Thank you for writing this

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

Than you for the kind words!
hopefully it helps people out :)

Collapse
lyndon_gingerich profile image
the-not-mad-psychologist

Excellent article, but the lack of proofreading makes it hard to read.

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

Thank you for taking the time time and effort to read it :)
I agree completely with you sometimes I wish I had more time to do these things but if I don't get them out when I finish those it is hard to know when I'll have time to publish them I will take more efforts to improve in this area, thank you!

Collapse
digitalstoic profile image
Mat • Edited on

Great article, thanks. 1 question about PWA (Progressive Web Application): Only the Fable-based solution allow the required low-level setup (service workers, data cache, etc), right?

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez Author

PWAs are agnostic to how the application is built or how it runs, so you can build PWAs from all of the tech mentioned on this post :)
that being said, of course if you use a toolchain that allows for more control over service workers and similar things (like webpack/vite which are usable on the fable side) it will be easier to include it in your source code in F#
otherwise you will have to write that other Javascript manually write your service worker in javascript like I did here

So if you choose websharper/fable/web assembly you can for sure

actually, here's a sample I wrote with bolero last year