Breath of Fresh Air with Solid JS and Fable.Solid

Explore SolidJS and Fable.Solid for next-gen web development. Discover fine-grained reactivity, atomic state management, and F# bindings.

10 min read 1,866 words

History

I have spent the better part of my engineering career developing web applications. From frameworks like JQuery to AngularJS to KnockoutJS to React everywhere.

These were many aha moments in history. But after ReactJS launched, there was no aha moment like that. Elm was there, but it couldn't beat the reach or impact of ReactJS.

One thing adopted from Elm land by all frameworks is The Elm Architecture. It is a great way to develop large-scale applications. Instead of following different state management frameworks, you rely on creating a specific style of architecture. It was something similar to the MVC style of developing backend applications.

The world seems to be frozen in one place. Things are working, there are no issues there. But as usual, developers tend to find bottlenecks sooner than later. These give rise to a couple of things. One is Atomic State Management and the other is fine-grained reactivity.

Both came from the requirement of performance and developer experience. React is good and getting better, but its internals don't allow it to go beyond a certain point. Then comes SolidJS. It has both fine-grained reactivity and atomic state management. It also has good things from ReactJS, like JSX and component-based development.

Let's talk about F# a bit. I don't have to write much in this blog, as you can read my journey in F# in my articles. Fable kind of has had official ReactJS bindings since inception. If you are looking for some next-gen options, then F# also has official WebAssembly support using FsBolero, a wrapper around Blazor.

SAFE and SAFEr stack

The F# community also came up with a stack to develop a full-stack application called the SAFE stack. It sets you up for Elm Architecture in F# with React as your rendering engine. You can use various frameworks as server applications. My favorite flavor of the SAFE stack is the SAFEr stack with either Giraffe or Serverless options. I personally like Giraffe—maybe I'm just old school. But in any way, I like SAFEr better than the original SAFE any day.

The Struggle

Fable + Elmish struggle when you are developing large-scale applications or quite complicated applications from a UI point of view. You pass the dispatch everywhere to update your state. The state is always immutable—it's a great thing to have—but never forget JavaScript is single-threaded, and updating the global state in a very nested object is a costly affair. Think if you are filling up forms deep down in the UI where you need to update the state for every keystroke you make. Then it will update a.b.c.d.e.f every time immutably and rerender that leaf and all its parents. Similar things happen for complex recursive UI. It works, but there will be a hit on performance and developer experience. Rendering and passing dispatch everywhere is not that great of an experience either. Here, even if you skip the ELM and stay true to React, there is the issue of prop drilling. Third-party state management comes with a cost of maintaining F# bindings, and if we go for context, then it will update the whole tree of UI when the state changes, so there's a cost on performance.

The above case will never arise for a simple application, but if a project is developed in F#, then simple is out of the question in most cases.

The Alfonso Garcia-Caro Answer

When I was struggling with all this, I got a suggestion to look into SolidJS as Alfonso announced the SolidJS binding. Alfonso does release a thing or two every year, so it's hard to keep up with his every new release. But when I looked at it, it was an aha moment for me. SolidJS brings back the beauty of KnockoutJS from the past and mixes it with ReactJS syntax. SolidJS is an awesome library that not only includes UI but also state management.

SolidJS might take a whole other blog if I write about it and the differences between React and SolidJS. I'll leave it to users to figure that out. But here I want to talk about Fable.Solid.

It is part of Fable 4. So currently it's in beta, alpha, and gamma style versions. It is becoming more and more API-complete, and as of now, it is not fully API-complete with SolidJS. But still, it works. Here is a code sample from the original repo. You can find more examples in the repo as well.

open System
open Fable.Core
open Feliz.JSX.Solid

open type Components

  type Components with
    [<JSX.Component>]
      static member 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!" ]
                      ]

                  Html.hr []
                  Components.DivideBy()

                  Html.hr []
                  Html.p "These components share state through context provided by the App parent"
                  Html.br []
                  Components.ColorInput()
                  Components.ColorInput()
              ]

Another thing that I liked even more than Fable.Solid is Fable.JSX coming for SolidJS and React both. It allows a developer to write HTML and JSX code inside F# code. It makes things way easier when designing applications. Now we don't have to convert every possible design element to F# code; instead, we can use HTML syntax as it is. It just works. Check out how you can easily use HeroIcons inside your Fable Solid application.

[<JSX.Component>]
let Plus() =
   JSX.html """
<svg
  xmlns="http://www.w3.org/2000/svg"
  fill="none"
  viewBox="0 0 24 24"
  stroke-width="1.5"
  stroke="currentColor"
  class="w-4 h-4 ml-3"
>
  <path
    stroke-linecap="round"
    stroke-linejoin="round"
    d="M12 4.5v15m7.5-7.5h-15"
  />
</svg>
 """

[<JSX.Component>]
let Funnel() =
   JSX.html """
<svg 
xmlns="http://www.w3.org/2000/svg" 
fill="none" 
viewBox="0 0 24 24" 
stroke-width="1.5" 
stroke="currentColor" 
class="w-4 h-4 mr-2"
>
  <path 
  stroke-linecap="round" 
  stroke-linejoin="round" 
  d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" 
  />
</svg>
"""

If you haven't felt excited about this, then just hold on for a second. Another great thing about Fable.JSX is the JSX import. Finally, it makes things easier to import any kind of JSX component in F#. It makes things easier for libraries like SolidJS which don't use a virtual DOM to create components, so they don't have any mechanism to create one. They just use JSX syntax to create a real DOM tree in HTML. See how easy it is when you are using a third-party library—you can write your code in TSX and import it in F# with minimum effort. As you can see, I'm just exposing data to be passed, and other configuration stays in TSX. So it stays near the reference code for that library, making things easier to manage and read for future me as well as anyone joining the project in the future.

import type { Component } from 'solid-js';
import AgGridSolid from 'ag-grid-solid';

import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';

export const Test: Component = ({data} : {
  data : {
    make : string;
    model : string;
    price : number;
  } []
}) => {
  const columnDefs = [
    { field: 'make' },
    { field: 'model' },
    { field: 'price' },
  ];

  

  const defaultColDef = {
    flex: 1,
  };

  return (
    <div class="ag-theme-alpine" style={{height : '600px'}}>
      <AgGridSolid
        columnDefs={columnDefs}
        rowData={data}
        defaultColDef={defaultColDef}
      />
    </div>
  );
};

with

type AgGridSolid =
  
  [<ImportMember("./AgGridSolid.tsx"); JSX.Component>]
  static member Test (data : {|make : string; model : string ; price : double |} []) : JSX.Element = jsNative

can be used like

  [<JSX.Component>]
  let AgGridView() =
    Html.div [
      Html.p "Ag Grid Should Come Here..."
      AgGridSolid.Test([|
        {|
          make = "Toyota"; model = "Celica" ; price = 35000. 
        |}
        {|
          make = "Ford"; model = "Modeo" ; price = 32000. 
        |}
        {|
          make = "Porsche"; model = "Boxter" ; price = 72000. 
        |}
      |])
    ]  

Let's dig deep into various examples of SolidJS.

Here is one with Signal, which is similar to React's useState. But the difference is that it will update only a piece of the DOM when data is changed, instead of rendering the whole DOM where useState is used.

[<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.h1 [
            Attr.className "text-3xl font-bold underline"
            Html.children [
                Html.text "Counter!!!"
            ]
        ]
        Html.button [
            Attr.className "button"
            Ev.onClick(fun _ -> count() + 1 |> setCount)
            Html.children [
                Html.text $"Click me!"
            ]
        ]
    ]

Now take the same example with Store. The code looks almost the same. According to the docs, one should use Signal when there is a need to update whole data at once, like a number or string. But when there is a need for partial updates, then use a store. Like arrays and objects should be stored in the store. It will update the entity with a partial update to that property. SolidJS will update the UI reactively. Without a virtual DOM, this would be way more performant compared to React.

type CounterStoreType = {
    Inner : int 
}

[<JSX.Component>]
let CounterStore() =
    let count, setCount = Solid.createStore<CounterStoreType>({
        Inner = 0
    })
    let doubled() = count.Value.Inner * 2
    let quadrupled() = doubled() * 2

    Html.fragment [
        Html.p $"{count.Value.Inner} * 2 = {doubled()}"
        Html.p $"{doubled()} * 2 = {quadrupled()}"
        Html.br []
        Html.h1 [
            Attr.className "text-3xl font-bold underline"
            Html.children [
                Html.text "Counter Store!!!"
            ]
        ]
        Html.br []
        Html.button [
            Attr.className "button"
            Ev.onClick(fun _ -> 
                setCount.Update {
                    Inner = count.Value.Inner + 1
                })
            Html.children [
                Html.text "Click me!"
            ]
        ]
    ]

The above example actually doesn't justify store use, but as you can see, it's very similar to how we use Signal.

What about state management when there's a need to share state between components? Parent-child is easy—just pass it using props like in React. But if you want to share state between siblings, then you can easily do it with context. Now, if you think context will update the whole DOM tree, then my friend, there's no virtual DOM, and fine-grained reactivity will update only what is required. So just put things in context and you'll get a sharable Store or Signal. Yup, no passing dispatch in props, and all child components of the context have access to the store. The API for context is also implemented in Fable.Solid. Check out how it works.

This birthday post is part of the FsAdvent calendar 2022. I'm sorry that I'm unable to write more nowadays as I'm busy with F# work. But I should be writing more and giving talks nationally and internationally in 2023. I'd like to thank the phosphor.co team for the continued support and especially Alfonso Garcia-Caro for his awesome work.

I'd like to request readers to suggest a new name for the stack: Giraffe + Solid + Whatever Cloud or Database. Something fresh for the future as well as for old F# web users.

Frequently Asked Questions

What is fine-grained reactivity and how does it differ from React?

Fine-grained reactivity is a performance optimization approach where only the specific DOM elements that depend on changed state are updated, rather than re-rendering entire components. React's component-based approach has internal limitations that prevent it from achieving this level of granular updates, which is why SolidJS was created to offer both fine-grained reactivity and atomic state management in a single framework.

What is the SAFE stack and why would I use it?

The SAFE stack is a full-stack web development framework built on F# that combines Elm Architecture principles with React for rendering and various backend frameworks. It's designed for developers who want type-safe, functional programming across their entire application while leveraging the benefits of Elm Architecture for state management.

What are the main challenges with Fable and Elmish for large applications?

Fable and Elmish struggle with large-scale or UI-complex applications primarily because you must pass `dispatch` throughout your component tree to update state, and maintaining a globally immutable state in a single-threaded JavaScript environment can become inefficient and difficult to manage.

How does SolidJS compare to React and why is it considered a breakthrough?

SolidJS represents a significant advancement by combining fine-grained reactivity with atomic state management while retaining popular React features like JSX and component-based development. Unlike React, SolidJS's architecture allows it to achieve better performance and developer experience by updating only the specific reactive dependencies that change.

What is the SAFEr stack and how is it different from the original SAFE stack?

The SAFEr stack is a modern variation of the SAFE stack that offers improved flexibility with options for either Giraffe or Serverless backend frameworks. The author prefers SAFEr over the original SAFE for its enhanced adaptability and cleaner configuration options.

Share this article