Fable + Solid: React, the good parts with a performance boost

Alfonso García-CaroOctober 18, 2022

When announcing Fable 4 we explained how the new JSX compilation target made Fable compatible with SolidJS. But what is SolidJS? The best way to answer this question is to check their website but in short, it's a tool/library that allows you to write React-like apps, while taking advantage of the JSX compilation step to analyze the dependencies in your code and transform your declarative UI into imperative statements that make localized DOM updates, instead of using a Virtual DOM and calculate the diffing at runtime as React does.

This approach was popularized by Svelte in the JS world, but it was also pioneered in F# by FSharp.Adaptive and Sutil. Some of the advantages are:

  • Better performance as the diffing is already done at compile time
  • Smaller bundle sizes because the framework doesn't need a big runtime
  • Components are "just functions" instead of instances in disguise

The third point has some implications for performance, but the most important consequence is the code within the component will behave as you expect from a regular function. I think this is important for F# developers, as the community has sometimes struggled to understand (and explain) well the difference between functional components and "just" functions. With Solid, the code is only executed when the function is called during the program flow. Consider the following example:

open Fable.Core

let Counter() =
    printfn "Evaluating function..."
    let count, setCount = Solid.createSignal (0)

    JSX.html $"""
        <p>Count is {let _ = printfn "Evaluating expression..." in count()}</p>
        <button class="button" onclick={fun _ -> count () + 1 |> setCount}>
            Click me!

The message appearing on the function root, "Evaluating function", is only printed once, while the local message, "Evaluating expression", is printed every time count gets updated. This means Solid has detected that, when count changes, it only needs to update the re-evaluate the expression in that particular JSX "hole", and it can leave the rest of the component untouched. And Solid is capable of doing this not only with the signal primitives, but also with props coming from a parent component or even an Elmish model!

To be fair, there is still some magic involved, as you need to understand the expressions containing a reactive value will be re-evaluated every time the value changes.

The UI in Solid apps looks very much like React ones, but you can choose between camelCase or "proper" attribute names, like class or autofocus. Another important difference is when declaring a dynamic list or conditional elements: in these cases Solid requires you to be explicit with For and Show/Switch respectively. For helpers to create reactive values, effects and such, please check Solid documentation and Fable.Solid bindings.

// Note how `Solid.For` is used to display the list of Todos
let model, dispatch = Solid.createElmishStore (init, update)

JSX.jsx $"""
    <p class="title">To-Do List</p>
    {InputField dispatch}
    <ul>{Solid.For(model.Todos, (fun todo _ -> TodoView todo dispatch))}</ul>

Solid is also compatible with Web Components. So if you want to use a library like Shoelace, you only need to register the components you need, and then invoke them as if they were native HTML elements. The only thing you need to remember is to prefix custom events with on:

open Fable.Core
open Fable.Core.JsInterop

let ImageComparer() =
    // Cherry-pick Shoelace image comparer element, see https://shoelace.style/components/image-comparer
    importSideEffects "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.73/dist/components/image-comparer/image-comparer.js"

    JSX.html $"""
        on:sl-change={fun (ev: Event) -> printfn "New position: %i" ev.target?position}>
        <img slot="before"
            alt="A person sitting on bricks wearing untied boots."></img>
        <img slot="after"
            alt="A person sitting on a yellow curb tying shoelaces on a boot."></img>

Latest Fable.Core provides JSX.html helper. This is an alias of JSX.jsx but it's useful if you're using the F# Template Highlighting VS Code extension and you prefer to identify the embededded language as HTML rather than JSX.

If you want to try Solid but still prefer F# for UIs, Feliz.JSX.Solid provides a Feliz-like API for that. And you can freely combine both approaches:

let QrCode() =
    importSideEffects "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.73/dist/components/qr-code/qr-code.js"
    let value, setValue = Solid.createSignal("https://shoelace.style/")

    Html.fragment [
        Html.input [
            Attr.className "input mb-5"
            Attr.autoFocus true
            Attr.value (value())
            Ev.onTextChange setValue
        Html.div [
            JSX.html $"""<sl-qr-code value={value()} radius="0.5"></sl-qr-code>"""

Likewise, if you want to try Solid but prefer to use Elmish to manage state, Elmish.Solid has you covered. Similarly to useElmish, createElmishStore allows you to use Elmish at the component level, but it takes advantage of Solid stores to compare the model snapshots and update only the updated parts. Check how the classic Elmish TodoMVC is rendered with Solid, in this example.

For Solid store diffing to work best, you should use arrays (instead of, say, lists or sets) for collections that correspond with on-screen elements.

One particular feature of createElmishStore is that it can keep state with Vite hot reload. I say "particular" because, at the time of writing, there's no equivalent of React's Fast Refresh and Solid primitive signals won't keep state while hot reloading. How is it possible to get better tooling with Fable/F# than with native JS? We will talk about this in the next post.