Getting started with Fable and Feliz

F#
Fable
Feliz

tl;dr: In this post we go from a plain F# console app and add Fable, Feliz and Vite and dodging some not needed extras like Paket, Femto or Elmish to stay beginner friendly and have less chances for problems.

Intro

When I started my F# journey two or so years ago, I was quite hooked by the language, its features and some extending technologies like Fable. The general sentiment of folks who try this thing is that they never want to go back (I might be one of those now).

However really getting started with a new thing, especially if it is quite different that what you are used to, takes some time. I wanted to relatively fast dive into more advanced stuff like Fable and Fable.React, but just jumping into cold waters was troublesome. I thought that when using a template or example application that shows what is possible I could go from there. However this approach had problems:

  • F# has quite a different syntax then C# or Java. When you try out a language with a similar paradigm (let's say from Java to C#), you will feel familiar and won't have too many problems. Switching from an object-oriented language to a function one however is difficult. Switching from C# to F# will first be uncomfortable. Less curyl braclets, white space is important, type annotations often not needed, pattern matching, computation expressions, pipings...
  • Usually F# tutorials or docs not only come with plain F#, but additional tooling (like Paket, Femto) and other architectural patterns (like Elmish) are added. These totally exist for a reason, however when learning a new thing these just add noise and add a complexicity and chances for errors that were frustrating.
  • Documentation/content: As the community is relatively small, there is not too much general F# content out there. I don't remember the Fable documentation at that time, now when revisiting it looks pretty good. However I am still missing walk throughs and bigger tutorials (so fixing this gap a little with this post).
  • Webpack: Some time ago Webpack was needed to run Fable.React. That also was a pain point for me. Nowadays there is Vite, which is way easier to setup. So one problem less overall.

So now with some experience in F# I am revisiting Fable want to get my hands dirty. Let`s get started with writing a web application that will run in the browser. I will go from zero, so all steps can be followed.

The following resources have been helpful when learning, so visit them if you want to learn more:

Initial setup

Before we begin you need the dotnet sdk and node installed. I am using node 18 and dotnet 7 when writing this post.

Lets create a simple F# console application, install fable as a dotnet tool and compile the Program.fs file to typescript:

dotnet new console -lang F# --name GettingStartedWithFable
cd GettingStartedWithFable
dotnet new tool-manifest
dotnet tool install fable
dotnet fable --lang ts

Voilà, if you have no errors than you might just have compiled some F# to Typescript!. Lets check the files that we have now:

./.config/dotnet-tools.json is our dotnet tool manifest file, containing the fable tool, that allows us to run dotnet fable to compile F# to some other language:

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "fable": {
      "version": "4.1.4",
      "commands": ["fable"]
    }
  }
}

GettingStartedWithFable.fsproj is our F# project file, referencing all F# files that need to be compiled:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <RootNamespace>getting_started_with_fable</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

Program.fs contains our F# code:

printfn "Hello from F#"

Program.fs.ts contains the resulting TypeScript code:

import { printf, toConsole } from "./fable_modules/fable-library-ts/String.js"

toConsole(printf("Hello from F#"))

There are other folders like ./bin/ ./fable_modules/ and ./obj/ that we can ignore.

Add vite

In order to run our code we can either compile it to JavaScript and execute with node, or we can use a bundler like webpack or vite. I will use vite in this case. Let's add a couple of files to our folder:

package.json for our frontend depenencies, scripts and other metadata:

{
  "name": "getting-started-with-fable",
  "version": "1.0.0",
  "description": "",
  "main": "Program.fs.js",
  "type": "module",
  "scripts": {
    "client": "vite",
    "tsc": "tsc"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "typescript": "5.1.6",
    "@types/node": "20.4.2",
    "vite": "4.4.3"
  }
}

tsconfig.json for our TypeScript compiler options:

{
  "exclude": ["fable_modules"],
  "compilerOptions": {
    "target": "es2022",
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true
  }
}

the target of "es2022" here is important, older options seem not to be supported by Fable.

index.html as our root html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Fable</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/Program.fs.ts"></script>
  </body>
</html>

vite.config.ts for our vite configuration:

import { defineConfig } from "vite"

// https://vitejs.dev/config/
export default defineConfig({
  clearScreen: false,
  server: {
    watch: {
      ignored: [
        "**/*.fs", // Don't watch F# files
      ],
    },
  },
})

When running

npm i
npm run client

Opening the website that is running on localhost we get a white screen. However when looking into the developer console we should see Hello from F#!

Add Fable.Browser.Dom

In order to update DOM, we can use the Fable.Browser.Dom package. Let's add it to our project and update Program.fs:

dotnet add package Fable.Browser.Dom
open Browser
let div = document.createElement "div"
div.innerHTML <- "Hello from F# and Fable!"
document.body.appendChild div |> ignore

Our GettingStartedWithFable.fsproj should now look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <RootNamespace>getting_started_with_fable</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Fable.Browser.Dom" Version="2.14.0" />
  </ItemGroup>

</Project>

Running dotnet fable --lang ts or dotnet fable watch --lang ts (to continuously watch for changes) will update our Program.fs.ts file to look like this:

export const div: HTMLElement = document.createElement("div")

div.innerHTML = "Hello from F# and Fable!"

document.body.appendChild(div)

And our website should now say Hello from F# and Fable! on the screen!

Cool. Now lets look at some things Fable.Browser.Dom gives us, to work with Javascript from F#

Fable.Browser.Dom

As seen in the last section, Fable.Browser.Dom gives us access to the document object. This is the same object that we would get in JavaScript. We can use it to create elements, query for elements, and do other things with the DOM. There are some more constructs that we can use to interact with JavaScript, like jsNative and Emit.

Emit

The Emit attribute allows us to write raw javascript/typescript. When added to a value, in this case x it means that whenever we use x in our F# code, it will be replaced with the value that was provided, in this case (5 as number). Note that this is not type-save, as you can put any string in here. So you must be careful:

open Browser
open Fable.Core

[<Emit("(5 as number)")>]
let x: obj = jsNative

let div = document.createElement "div"
div.innerHTML <- "Hello world!" + x.ToString()
document.body.appendChild div |> ignore

Results in:

import { toString } from "./fable_modules/fable-library-ts/Types.js"

export const div: HTMLElement = document.createElement("div")

div.innerHTML = "Hello world!" + toString(5 as number)

document.body.appendChild(div)

jsNative

The jsNative value that we have seen, is a value that is never executed. If it would be executed an exception would be raised (this can happen if you try to run the F# code directly, and not compile it to another language).

jsNative can be used when we want to use some JavaScript code that we know is or will be there at runtime. In the previous case we know the Emit attribute will provide the JavaScript wherever x is used. In other cases, for example if we want to call some JavaScript code that we know or expect to be there, we also can use jsNative as we see in the next example.

Import

Let's add a new file hello.js that is just plain JavaScript:

export function hello() {
  console.log("Hello from JS")
}

And in our Program.fs we can now import this function with the Import attribute:

open Browser
open Fable.Core

[<Import("hello", "./hello.js")>]
let hello: unit -> unit = jsNative

hello ()

Note that we also typed the function to be unit -> unit. This is because we know the function to not take any arguments and it does not return anything. Afterwards the function can be called as if it was a normal F# function.

Generated code:

import { hello } from "./hello.js"
hello()

If we would want to import from other packages, we just need them to package.json, add imports and type the imports accordingly. The Fable ecosystem also as packages that provide bindings for popular JavaScript/TypeScript libraries.

Other Fable JavaScript features including other ways on how to import JavaScript and interacting with the Global object can be found on the Fable website.

Add Feliz

Now let's add the Feliz package in order to write React code in F#:

dotnet add package Feliz
npm i react react-dom @types/react
open Browser
open Feliz

[<ReactComponent>]
let Counter () =
    let (count, setCount) = React.useState (0)

    Html.div
        [ Html.h1 count
          Html.button [ prop.text "Increment"; prop.onClick (fun _ -> setCount (count + 1)) ] ]


ReactDOM.render (Counter(), document.getElementById "root")

Feliz provides basically a DSL that looks similar to React.

Add a third party library

Let's add a third party library to our frontend that gives us some nice ui components to work with:

npm i "@mantine/core"

To use these components in our F# code we need to first import them.

open Browser
open Feliz
open Fable.Core.JsInterop

module Mantine =
    let TextInput props =
        Interop.reactApi.createElement (import "TextInput" "@mantine/core", createObj props)

open Mantine

[<ReactComponent>]
let Counter () =
    Html.div [ TextInput [ "label" ==> "First Name"
                           "placeholder" ==> "Enter your first name..." ] ]


ReactDOM.render (Counter(), document.getElementById "root")

The Mantine.TextInput value has the type seq<string*obj> -> Fable.React.ReactElement. So it is a function that takes a list (or sequence to be exact) of properties and returns a ReactElement. The properties are tuples of strings and objects. The string is the name of the prop, and the object the value. The ==> operator is a helper function that creates a tuple.

Fable.Core.JsInterop.createObj is a helper function that creates a JavaScript object from a list of tuples. The first value of the tuple is the key, and the second the value.

Interop.reactApi.createElement is a helper function from Feliz.

The resulting TypeScript code looks like this:

import { Interop_reactApi } from "./fable_modules/Feliz.2.6.0/Interop.fs.js"
import { TextInput } from "@mantine/core"
import { createObj } from "./fable_modules/fable-library-ts/Util.js"
import { ReactElement } from "./fable_modules/Fable.React.Types.18.3.0/Fable.React.fs.js"
import { createElement } from "react"
import React from "react"
import { FSharpList, singleton } from "./fable_modules/fable-library-ts/List.js"
import { Interop_reactApi as Interop_reactApi_1 } from "./fable_modules/Feliz.2.6.0/./Interop.fs.js"
import { render } from "react-dom"

export function Mantine_TextInput(
  props: Iterable<[string, any]>
): ReactElement {
  return Interop_reactApi.createElement(TextInput, createObj(props))
}

export function Counter(): ReactElement {
  const children: FSharpList<ReactElement> = singleton(
    Mantine_TextInput([
      ["label", "First Name"] as [string, any],
      ["placeholder", "Enter your first name..."] as [string, any],
    ])
  )
  return createElement<any>("div", {
    children: Interop_reactApi_1.Children.toArray(Array.from(children)),
  })
}

render<void>(createElement(Counter, null), document.getElementById("root"))

It is not your typical react code that we would write ourselves, but JSX would also be compiled to something similar.

Other mantine components now could be added in a similar fashion:

open Browser
open Feliz
open Fable.Core.JsInterop

module Mantine =
    let TextInput props =
        Interop.reactApi.createElement (import "TextInput" "@mantine/core", createObj props)

    let Space props =
        Interop.reactApi.createElement (import "Space" "@mantine/core", createObj props)

    let AppShell props =
        Interop.reactApi.createElement (import "AppShell" "@mantine/core", createObj props)
//...

Note that the props parameter can be typed by ourselves. Similar to previously, where we typed the hello function. So we could create a type for each component, for example as a DiscriminatedUnion and then only allowing valid values:

open Browser
open Feliz
open Fable.Core.JsInterop

module Mantine =
    type props =
        | Label of string
        | Placeholder of string

    let TextInput (props: props seq) =
        let props =
            props
            |> Seq.map (function
                | Label label -> "label" ==> label
                | Placeholder placeholder -> "placeholder" ==> placeholder)

        Interop.reactApi.createElement (import "TextInput" "@mantine/core", createObj props)

open Mantine

[<ReactComponent>]
let Counter () =
    Html.div [ TextInput [ props.Label "First Name"
                           props.Placeholder "Enter your first name..." ] ]

The process of typing the props is a bit cumbersome, but I guess it is relatively straight forward. Also the ts2fable tool can be used to generate some Fable/F# code based of TypeScript defintions.

Though this is not perfect I think the benefits that F# gives maybe outweight the extra work to type the api (or loosing strong typing).

However recently - and as demonstrated in this blogpost - Fable can generate TypeScript, which was not always the case. And with this, I think a hybrid approach of generated F#/Fable and normal TypeScript/React is much more efficient. Also if I am honest, looking at the Feliz code it looks not as nice as normal React code:

import { AppShell, Space, TextInput } from "@mantine/core"
import * as React from "react"

export function Counter() {
  const [count, setCount] = React.useState(0)
  return (
    <div>
      <AppShell>
        <div>
          <TextInput label="Name" placeholder="Enter your name.." />
          <Space my="xs" />
          <TextInput label="Email" placeholder="Enter your email..." />
        </div>
      </AppShell>
    </div>
  )
}

I think this code is much simpler to read (maybe as it is well known). The formatter (prettier) works better (so we get better tooling native to the ecosystem) and we get native autocomplemetion and typesafety via typescript without writing bindings. The full js/ts/react ecosystem is now easily accessible.

However we dont want to remove F# completly. I think for interacting with the ecosystem this style is better, but when we write or own normal code, that is not view related, we can use F# and all its goodness: