Fastoch.Server 0.7.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Fastoch.Server --version 0.7.0
                    
NuGet\Install-Package Fastoch.Server -Version 0.7.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.Server" Version="0.7.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Fastoch.Server" Version="0.7.0" />
                    
Directory.Packages.props
<PackageReference Include="Fastoch.Server" />
                    
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.Server --version 0.7.0
                    
#r "nuget: Fastoch.Server, 0.7.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.Server@0.7.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.Server&version=0.7.0
                    
Install as a Cake Addin
#tool nuget:?package=Fastoch.Server&version=0.7.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 .

Server side rendering

Fastoch.Feliz Html DSL (Domain Specific language) can also be used server side to generate html.

let page =
    Html.html [
        Html.head [
            Html.title "Fastoch"
        ]
        Html.body [
            Html.h1 "Fastoch"
        ]
    ]

let html = Fastoch.Feliz.Server.render page

printfn "%s" html

The output is:

<!DOCTYPE html>
<html><head><title>Fastoch</title></head><body><h1>Fastoch</h1></body></html>

Rendering can be tweaked using the renderWithOptions function.

It is also possible to write the output directly to a System.IO.TextWriter with the write and writeWithOptions functions.

Events

Events are not rendered server side (this would require a conversion to javascript that is not implemented.)

Server/Client

It is possible to share a view between server and client using a shared assembly.

namespace Shared

open Fastoch.Feliz

type Model =
    { Count: int
      Loading: bool
    }

type Command =
| Done
| Increase
| Reset

module View =
    let view model dispatch =
        Html.div [
            prop.children [
                Html.text model.Count
                Html.button [
                    prop.text "Increase"
                    prop.onClick (fun _ -> dispatch Increase)
                    prop.disabled model.Loading
                ]
                Html.button [
                    prop.text "Reset"
                    prop.onClick (fun _ -> dispatch Reset)
                    prop.disabled model.Loading
                ]
            ]
        ]

This can be used for the elmish client part:

open Elmish
open Fastoch.Elmish.HMR
open Shared


let init count =
    { Count = count
      Loading = false
    }, Cmd.none

let update cmd model =
    match cmd with
    | Increase ->
        { model with Count = model.Count + 1; Loading = true}, 
            Cmd.OfAsync.perform ( fun () -> async {
                let! _ = Fable.SimpleHttp.Http.post "/api/increase" "" 
                ()
            }) () (fun _ -> Done)

    | Reset ->
        { model with Count = 0; Loading = true },
            Cmd.OfAsync.perform ( fun () -> async {
                let! _ = Fable.SimpleHttp.Http.post "/api/reset" "" 
                ()
            }) () (fun _ -> Done)
    | Done -> { model with Loading = false }, Cmd.none

let loadCounter() =
    async {
            match! Fable.SimpleHttp.Http.get "/api/counter" with
            | 200, v -> return ((int v))
            | _ -> return 0}


async {
    // load the counter current value.
    // no problem if it's not instant as the page returned by
    // the server will already display everything correctly
    let! c = loadCounter()

    // start the elmish program with this initial value
    Program.mkProgram init update View.view
    |> Program.withFastoch "app"
    |> Program.runWith c
} |> Async.StartImmediate

On the server side, the view can be used to return the html page filled with initial data:

open System
open Microsoft.AspNetCore.Builder
open System.Threading.Tasks
open Fastoch.Feliz
open Shared

let app = WebApplication.Create();

app.UseWebSockets() |> ignore

// dispatch will not be used server side...
// just ignore the given command 
let dispatch = ignore 

// The full page Html, using the view
let page model =
    Html.html [
        Html.head [
            Html.title "Counter"
            Html.script [
                prop.src "App.js"
                prop.type' "module"
            ]
        ]
        Html.body [
            Html.h1 "Counter"

            Html.div [
                // this div will be the root of the client side Fastoch element
                prop.id "app"
                prop.children [
                     View.view model dispatch
                ]
            ]
        ]
    ]

let mutable state = 0

app.MapGet("/", fun context  ->
    task {
        context.Response.ContentType <- "text/html"

        // render the page in loading state
        // it will switch to loaded state in the client init function.
        use writer = new IO.StreamWriter(context.Response.Body)
        Server.write writer (page { Count = state; Loading = true })
        do! writer.FlushAsync()
    } : Task) |> ignore




app.MapGet("/api/counter",Func<Task<int>>(fun () -> task { 
        // fake delay to show that the page is correctly displayed
        // before the script actually get the value
        do! Task.Delay (TimeSpan.FromSeconds 3.)
        return state })) |> ignore

app.MapPost("/api/increase",Action(fun () -> state <- state + 1)) |> ignore

app.MapPost("/api/reset",Action(fun () -> state <- 0)) |> ignore

app.MapGet("/App.js", fun context ->
    task {
        context.Response.ContentType <- "application/javascript"

        use stream = IO.File.OpenRead(@"..\dist\assets\App.js")
        do! stream.CopyToAsync(context.Response.Body)
    } : Task
) |> ignore

app.Run()

The page HTML can be rendered immediatly. Interactivity starts as soon as the javascript runs and the initial value is fetched from server.

It is also possible to avoid fetching the value from the server and pass it via a rendered script. In the client, run the program inside a start function with needed information:

let start count =
    Program.mkProgram init update View.view
    |> Program.withFastoch "app"
    |> Program.runWith count

Server side, in the page HTML template, import the start function and call it with the initial state:

let page model =
    Html.html [
        Html.head [
            Html.title "Counter"
            Html.script [
                prop.type' "module"
                // import the start function and call it with the count
                prop.children [
                    Html.text "import { start } from './App.js';"
                    Html.textf "start(%d);" model.Count
                ]
            ]
        ]
        Html.body [
            Html.h1 "Counter"

            Html.div [
                prop.id "app"
                prop.children [
                     View.view model dispatch
                ]
            ]
        ]
    ]

This way, there is no need to load the counter back from server on start. Still, the page can be displayed correctly before the script is loaded and executed.

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 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.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.2 27 8/1/2025
0.7.1 47 8/1/2025
0.7.0 49 8/1/2025
0.6.0 87 7/31/2025
0.6.0-beta3 87 7/31/2025
0.6.0-beta2 83 7/31/2025
0.6.0-beta 87 7/30/2025