Fastoch.Server
0.7.0
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
<PackageReference Include="Fastoch.Server" Version="0.7.0" />
<PackageVersion Include="Fastoch.Server" Version="0.7.0" />
<PackageReference Include="Fastoch.Server" />
paket add Fastoch.Server --version 0.7.0
#r "nuget: Fastoch.Server, 0.7.0"
#:package Fastoch.Server@0.7.0
#addin nuget:?package=Fastoch.Server&version=0.7.0
#tool nuget:?package=Fastoch.Server&version=0.7.0
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 | Versions 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. |
-
net9.0
- Fastoch (>= 0.7.0)
- FSharp.Core (>= 9.0.300)
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 |