Fastoch 0.10.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Fastoch --version 0.10.0
                    
NuGet\Install-Package Fastoch -Version 0.10.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Fastoch" Version="0.10.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Fastoch" Version="0.10.0" />
                    
Directory.Packages.props
<PackageReference Include="Fastoch" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Fastoch --version 0.10.0
                    
#r "nuget: Fastoch, 0.10.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Fastoch@0.10.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Fastoch&version=0.10.0
                    
Install as a Cake Addin
#tool nuget:?package=Fastoch&version=0.10.0
                    
Install as a Cake Tool

Fastoch !

Fastoch is a virtual DOM for Elmish with minimal dependencies.

What does it mean ?

Fastoche (fastɔʃ, reads like 'fast-osh') means 'easy peasy' in French. Thank you Emmanuelle for the title. I removed the silent 'e' to make it more compact.

Why Fastoch ?

Elmish is a great model to develop robust web applications in F#.

However, having a dependency on React has three main drawbacks:

  • React does more than what is needed by Elmish, and is a bit heavy for the job.
  • React must be added using npm as is it not a Fable component.
  • React is developed by Facebook which is an awful company lead by a masculinist billionaire CEO.

This last point especially was a problem since I'm working on the online version of Transmission(s), a board game against discriminations and sexist violences in public spaces.

How to use it ?

Create a console F# project with fable dotnet tool

mkdir Sample
cd Sample
# create the console app
dotnet new console -lang F#
# add fable as dotnet tool
dotnet new tool-manifest
dotnet tool install fable

Add the Fastoch nuget to your project:

dotnet add package Fastoch

Fastoch copies the Feliz model to build HTML views, and is based on Elmish for the Model View Update part.

The only differences are the namespaces to open, and the function to call on Program to start it.

Write your elmish application in the Program.fs file:

open Elmish
open Fastoch.Feliz
open Fastoch.Elmish

type Model = 
    { Counter: int}

type Action =
    | Increment
    | Decrement
    | Reset

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

let update cmd model =
    match cmd with
    | Increment -> { model with Counter = model.Counter + 1}, Cmd.none
    | Decrement -> { model with Counter = model.Counter - 1 |> max 0}, Cmd.none
    | Reset -> { model with Counter = 0}, Cmd.none

let view model dispatch =
    Html.div [
        Html.ul [
            Html.li [
                
                prop.text   $"{model.Counter}"
                if model.Counter = 0 then
                    prop.style [ style.color "green"]
                elif model.Counter >= 10 then
                    prop.style [ style.color "red" ]
            ]
        ]
        Html.button [
            prop.text "+"
            prop.onClick (fun _ -> dispatch Increment )
        ]
        Html.button [
            prop.text "-"
            prop.onClick (fun _ -> dispatch Decrement )
        ]
        Html.button [
            prop.text "Reset"
            prop.onClick (fun _ -> dispatch Reset )
        ]
    ]

Program.mkProgram init update view 
|> Program.withFastoch "app" // here we use: withFastoch
|> Program.run

Add a index.html file to your project:

<!DOCTYPE html>
<html>
    <head>
        <title>Fastoch</title>
        <script src="Program.fs.js" type="module"></script>
    </head>
    <body>
        <div id="app"></div>

    </body>
</html>

Now run it with fable watch and vite:

dotnet fable watch . --run vite .

Et voilà, Fastoch!

Hot Module Replacement (HMR)

Fastoch implements directly HMR.

Without HMR, on code change, fable will emit new js files, vite will reload the application, and the application will restart from initial state like after pressing F5.

With HMR, the state will be persisted between reloads, providing the best development workflow.

To enable HMR Open the Fastoch.Elmish.HMR namespace instead of Fastoch.Elmish:

open Elmish
open Fastoch.Feliz
open Fastoch.Elmish.HMR

HMR is only active in debug, and all specific code will disapear in release mode.

Now run it again with fable watch and vite:

dotnet fable watch . --run vite .

Fable watch mode runs in debug by default, so HMR will be active Change the counter value, and edit the view. Changes are reflected instantly without affecting the counter value.

When building the project with fable, the default mode is release, so HMR code will not be emited.

dotnet fable .
vite build .

prop.key

When generating a list of child elements, the diff algorithm will try to match child elements of a common parent at the same index by default.

let countdown n =
    Html.ul [
        for i in n .. -1 .. 1 do
            Html.li i
    ]

// countdown 2 returns
// <ul><li>2</li><li>1</li></ul>
// countdown 3 returns
// <ul><li>3</li><li>2</li><li>1</li></ul>

countdown 2 will produce elements 2, 1, while countdown 3 will produce element 3,2,1. The diff algorithm will patch the original element with value 2 to change it to 3, the original element 1 to change it to 2, and append an new element 1 at the end.

Of course it's not optimal, it would be better to insert element 3 at the begining.

To help the algorithm keeping track of items while inserting/deleting item from a list or reordering the list, it is possible to specify keys on the elements:

let countdown n =
    Html.ul [
        for i in n .. -1 .. 1 do
            Html.li [
                prop.text i
                prop.key i
            ]
    ]

By specifying the key, the diff algorithm will consider that element with the same key are the same, here it will see that element 2 and 1 moved 1 place further but have no change, and the element 3 has no matching element and must be inserted.

This is also very useful to minimize diff on lists that can be reordered.

Keys only need to be local to their parent.

Html.thunk

Sometimes a part of the HTML tree to render contains a large number or nodes but actually changes rarely. The diff algorithm will walk all the tree to find no differences on most rendering, this is quite inefective.

To avoid re-rendering and doing the diff each time, it is possible to use Html.thunk:

let stars n =
    Html.thunk(n, fun () -> 
        Html.div [
            for i in 1..n do
                Html.span [ 
                    prop.className "star"
                    prop.text "*"
                ]   
        ])

Whithout Html.thunk, as long as n doesn't change, the div and the span nodes will be created on each view rendering and the diff will be made with the previous structure to find that there is nothing that changed.

With Html.thunk , the function is called the first time, but on next rendering, Fastoch will see that the value n, passed as first parameter, did not change, so there is nothing to patch in the subtree. The div and span virtual dom nodes are not created, and no further diff is performed.

dependencies

The first parameter is called dependencies. It is compared with previous value to know if the thunk should be reevaluated.

As F# implements structural equality, it's possible to pass a tuple, an array or any F# object as dependencies when the thunk depends on multiple values.

thunk key

As Fastoch is stateless, it will not match a thunk with it's previous version by reference, but by possition in its parent. As long as the position doesn't change, the diff algorithm will find previous version of the thunk at the same place, compare the dependencies, and if they are equal, skip the thunk entirely.

However, if the position of the thunk in the parent changes, the diff algorithm will try to compare the thunk with another element which is not a thunk, or a different thunk, and a rendering will occur.

let view dipatch model =
    Html.div [
        if model.Loading then
            Html.span "Loading"
        
        Html.thunk(model.Stars, fun () -> 
            Html.div [
                for i in 1..model.Stars do
                    Html.span [ 
                        prop.className "star"
                        prop.text "*"
                    ]   
            ])
    ]

In this example, depending on the Loading model property, the thunk can be in first or second position in its parent. When Loading is initialy false, the thunk is at index 0. Then when Loading becomes true, the loading span is at index 0, and the thunk at index 1.

The diff algorithm will compare the old thunk at index 0 with the new span, and decide it must be replaced by the span, then at index 1 see that the new thunk has no matching thunk in previous view, and render it from scratch.

To avoid this situation, Html.thunk can also take a key, in the same way that other elements use prop.key. This key can be passed as a second argument to Html.thunk

    Html.div [
        if model.Loading then
            Html.span "Loading"
        
        Html.thunk(model.Stars, "stars", fun () -> 
            Html.div [
                for i in 1..model.Stars do
                    Html.span [ 
                        prop.className "star"
                        prop.text "*"
                    ]   
            ])
    ]

When doing the diff, the algorithm will compare the two thunks with the "stars" key, and find that nothing changed, an just insert the loading span at the start of the parent.

Don't use the same key for a thunk and another sibling element, they're used the same way by the diff algorithm, and it would confuse it.

Hooks.callback to reduce event handler patching

In the previous example, we used prop.onClick (fun _ -> dispatch Increment ). The issue with such construct is that it creates a new callback on each rendering that is then used to patch the HTML DOM every time.

For a small application, this can be acceptable, but as the number of event handler grows, it can become a performance problem.

To avoid this, the Hooks.callback function creates a wrapper that will return the same function as long as the provided dependencies value is the same.

For instance:


let view dispatch =
    fun model ->
    Html.div [
        Html.ul [
            Html.li [
                prop.text   $"{model.Counter}"
                if model.Counter = 0 then
                    prop.style [ style.color "green"]
                elif model.Counter >= 10 then
                    prop.style [ style.color "red" ]
            ]
        ]
        Html.button [
            prop.text "+"
            prop.onClick (Hooks.callback((), fun _ -> dispatch Increment ))
        ]
        Html.button [
            prop.text "-"
            prop.onClick (Hooks.callback((), fun _ -> dispatch Decrement ))
        ]
        Html.button [
            prop.text "Reset"
            prop.onClick (Hooks.callback((), fun _ -> dispatch Reset ))
        ]
    ]

Here Hooks.callback it is called with () because id depends on no external value, and the actual function.

If the event handlers captures a value, it should be passed as a first argument so that the handler is updated each time the value changes. In case of multiple captures, the values can be passed as a tuple.

Hooks.effect

Effect hooks call a function when the component is created, and when the component is destroyed:

let divWithHook dep =
    Html.div [
        prop.text "Hello"
        prop.hook "once" (Hooks.effect(dep, fun element name ->
                    (console.log $"Hook {n} {dep}"
                        fun () ->  console.log $"Unhook {n} {dep}")))
    ]

The first parameter is the dependencies. The hook will be considered changed when the dependencies change. Dependencies are values used inside the hook.

The second parameter is the hook function that is called when the component is first created. element is the Element on which the hook is attached, the name is the name of the property (here "once" ). The function returns a unit -> unit function that will be called when the component is removed from the tree.

Thanks

Thank you Zaid Ajaj for Feliz. All the Html DSL is totally similar, and simply adapted to Fastoch virtual DOM. All code in Fastoch.Feliz namespaces is directly adapted from Feliz. Feliz is under MIT License

The Elmish directory is a patched version of Elmish to invert the order of model and disptach in view. The dispatch function is actually always the same and does not change each time the model change. By inverting the parameters, it is possible to partially apply the dispatch function. Elmish is under Apache-2.0 License

The HMR code is imported from Fable.Elmish.HMR do get rid of the indirect React dependency. Fable.Elmish.HMR is under Apache-2.0 License

The Virtual Dom implementation has been tweaked and converted to F# from Matt Esch original implementation.

This project is 100% AI free.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Fastoch:

Package Downloads
Fastoch.Server

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.11.0 0 3/13/2026
0.10.0 0 3/13/2026
0.9.3-beta1 30 3/11/2026
0.9.2 74 3/10/2026
0.9.1 67 3/10/2026
0.9.0 79 3/10/2026
0.8.1 88 3/7/2026
0.8.0 89 3/5/2026
0.7.3 96 2/27/2026
0.7.2 148 8/1/2025
0.7.1 143 8/1/2025
0.7.0 153 8/1/2025
0.6.0 190 7/31/2025
0.6.0-beta3 179 7/31/2025
0.6.0-beta2 174 7/31/2025
0.6.0-beta 180 7/30/2025
0.5.2 187 7/27/2025
0.5.1 186 7/27/2025
0.5.0 193 7/27/2025
0.5.0-beta2 135 6/20/2025
Loading failed