Getting started with Fable and Feliz

July 24, 2023

You can also use the Feliz template to get a similar result of what we are going to do here, however this post is intentially showing the steps to manually set up Feliz and explain some things on the go.

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. I thought that when using a template or example application that shows what is possible plus some F# beginner tutorials 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 complexity 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.

Now after quite some time revisiting, getting somewhere with Fable and Feliz does not seem to hard anymore, however the documentation is still a bit scattered and I didnt see a good walkthrough to get started with Feliz, without prior knowledge about Fable.

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:

.config/dotnet-tools.json
Copy

{
"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:

GettingStartedWithFable.fsproj
Copy

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

Program.fs
Copy

printfn "Hello from F#"

Program.fs.ts contains the resulting TypeScript code:

Program.fs.ts
Copy

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:

package.json
Copy

{
"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:

tsconfig.json
Copy

{
"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:

index.html
Copy

<!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:

vite.config.ts
Copy

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 interact with the 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

Program.fs
Copy

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:

GettingStartedWithFable.fsproj
Copy

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

Program.fs.ts
Copy

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

In my experiments watching for changes often did not work. So just running dotnet fable --lang ts usually worked better.

Fable.Browser.Dom

As seen in the last section, Fable.Browser.Dom gives us access to the document object to interact with the DOM. There are some more constructs that we can use to interact with JavaScript, like jsNative and Emit which we will look at in the next section.

Emit

The Emit attribute allows us to write raw javascript/typescript:


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

The attribute is added to a value, so that it can be used in F# code. You should give the value a type that best represents the given JavaScript/TypeScript code. Whenever this value is used it will be replaced with the given JavaScript/TypeScript code during compilation.

For example:

Program.fs
Copy

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:

Program.fs.ts
Copy

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)

Note that this is not typesafe, as you can put any string into the Emit attribute.

jsNative

The jsNative value that we have seen, is a value that is never evaluated. If it would be, 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.

Writing native JavaScript/Typescript and use it in F#

One cool thing about Fable is that in the end we just run JavaScript/TypeScript (or whatever language we are targeting), and it is possible to add native code ourselves and use it in F#.

Let's add a new file hello.js that is just plain JavaScript (we could also write TypeScript, but it would not make a big difference):

hello.js
Copy

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

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


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

Note that we also typed the function to be unit -> unit, which should represent the given JavaScript code very well. Note again that this is not type-safe. We ourselves decide what type we want to use here. Afterwards the function can be called as if it was a normal F# function.

Generated code:

Program.fs.ts
Copy

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

For some popular packages there exists Fable bindings. So others already have typed the packages for us, and we can use them in a type-safe style.

Besides importing by attribute, it is also possible to use the function import which can be used if we open Fable.Core.JsInterop.

Program.fs
Copy

open Fable.Core.JsInterop
let hello: unit -> unit = import "hello" "./hello.js"
hello ()

Results in

Program.fs.ts
Copy

import { hello } from "./hello.js"
export const hello: () => void = hello
hello()

It looks slightly different but essentially it is the same.

If we would want to use a published npm package, we just need to add them to package.json, run npm install and add an import similar to how we did earlier, but use the package name for the path (same concept as in JavaScript):

Program.fs
Copy

open Fable.Core.JsInterop
let hello: unit -> unit = import "hello" "some-package-name"
hello ()

Having some understanding of Emit, jsNative and Import should give us enough knowledge to practically use Fable.

Other Fable JavaScript/TypeScript features like 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

Program.fs
Copy

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 and comes with typings for all native html/react components (like div, span, h1, button and css styles). The code here is equivalent to:


import React from "react"
import { render } from "react-dom"
function Counter() {
const [count, setCount] = React.useState(0)
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
render(<Counter />, document.getElementById("root"))

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"

In normal react/jsx code, after importing we can just instantiate a component and pass it some props. With Feliz we usually use a helper function, that is named after the component has one parameter, that is a list of props. This helper function then instantiates the component with Feliz.Interop.reactApi.createElement and does some transformation on the props so that the component is instantiated correctly:


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")

createElement comes from Feliz, and it is they way to instantiate a React component. It takes a tuple, where the first element is the imported compment and the second element is the props. The props here need to be a plain JavaScript object. In order to create such an object, we can use the createOb from Fable, which takes a sequence of key value pairs, where the key is the prop name, and the value is the value to be passed to the prop. The ==> operator creates a tuple from its two arguments. So this code


TextInput [ "label" ==> "First Name"
"placeholder" ==> "Enter your first name..." ]

is the same as


TextInput [ ("label", "First Name"); ("placeholder", "Enter your first name...") ]

The createObj inside our helper function then constructs a JavaScript object from these tuples:


{ label: "First Name", placeholder: "Enter your first name..." }

which is passed to the component, the same way as it would be done in normal React (not in JSX, but the resulting plain JavaScript).

This code


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")

will be compiled by Fable into

Program.fs.ts
Copy

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 we didn't yet type props parameter. Currently it is a sequence of key value pairs, where the key is of type string, and the value is obj (props: seq<string*obj>). This is not yet type safe, as we could pass any string as key - even if this prop does not exist -, and give it any value - even though only specific types could be allowed.

If we want to only allow setting Label and Placeholder, and define that both values are of type string, we could do the following:

Program.fs
Copy

open Browser
open Feliz
open Fable.Core.JsInterop
module Mantine =
// strongly typed props
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..." ] ]

Other props can be added in a similar way. Note that we don't need to use the exact same types our library uses. Instead we can use the F# type system and transfrom these in our helper function so that it matches the expected types of the library.

A simpler way to import components was mentioned by Zaid, the author of Feliz:


open Fable
open Feliz
open Fable.Core
open Browser
[<Erase>]
type Mantine() =
[<ReactComponent(import = "TextInput", from = "@mantine/core")>]
static member TextInput(?placeholder: string, ?label: string) = React.imported ()
[<ReactComponent>]
let Counter () =
Html.div [ Mantine.TextInput(label = "First Name", placeholder = "Enter your first name...") ]
ReactDOM.render (Counter(), document.getElementById "root")

This looks better. Only if we want to use custom prop types that not exactly match the components props, I would use the earlier mentioned approach, where we have the chance to change the props in our wrapper function.

In order to speed up the process of typing props, we can use the ts2fable tool, which can generate F# code from TypeScript definitions. The generated code will not be perfect, at least with the mantine package the generated code was pretty broken, but it will give a good starting point.

As shown in this post, Fable allows to generate TypeScript code. So it is also possible to create a hybrid solution, where we use Fable and "native" TypeScript/React or JavaScript side by side. In the next post we will explore this option.