Using F# to build React apps: Fable
This is the first post in a series about how to build React apps using F#. In it, we'll cover a few topics:
- Generating JS from F# (this post)
- Writing React code from F#
- Using npm packages from F#
- Using the Elm architecture in your React components
In this post, we're focusing on why you might like F# and how you can use it to write code that runs in the browser. Let's dive in!
Generating JS from F# with Fable
Fable is a tool used to convert F# into other languages. It is mostly used to convert to JS so that is where it is most mature, but other languages are available (including TypeScript). Since this series is focused on React which is written in JavaScript, we will use Fable to convert our F# into JavaScript.
Why use F#?
- Well-designed language (no weird stuff like variable hoisting, unwanted type coercion, etc.)
- Allows combining data easily:
- Records - similar to objects but immutable
- Discriminated Unions (DUs) - allow easy modeling of a choice from a constrained list. One of F#'s most-loved features!
- Has a powerful static type system - in F# if it compiles it will probably "just work™"
- Simple and elegant syntax
Read our blog posts on why we love F# and Fable for a more detailed look at what specifically makes F# so great.
Getting started with Fable
Requirements:
- .NET SDK version 8 (ideally)
- Node version 18+ or 20+
First off let's install Fable (a .NET tool).
mkdir my-project
cd my-project
dotnet tool install fable --create-manifest-if-needed
Next, let's create a package.json
file and then install Vite. We will use Vite for its dev server to serve up our pages.
npm init -y
npm install -D vite
We need to create a webpage in the form of a new file: index.html
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fable Experiments</title>
<script type="module" src="output/app.js"></script>
</head>
<body>
<p id="greeting">Hello.</p>
<button id="clickme">click me!</button>
</body>
</html>
Don't worry that output/app.js
doesn't exist yet - we will create that very soon.
Let's get Vite up and running right away and then we can work on the JavaScript. Vite's dev server will serve up index.html
by default.
npx vite
Output:
VITE v5.4.2 ready in 111 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help
We can try accessing the site right now by pointing our browser to http://localhost:5173/
Now that that's set up, let's write some F# code!
F# doesn't have any way to interact with the browser by default, but Fable provides libraries so that we can interact with the browser in the same ways that you normally would from JavaScript. An example of such a library is Fable.Browser.Dom
, which enables searching for and manipulating DOM elements.
#r "nuget: Fable.Browser.Dom"
open Browser
type UserTier =
| Standard of userid:int
| VIP of name:string
let greetingElement = document.getElementById "greeting"
let clickmeElement = document.getElementById "clickme"
let greet user =
let greeting =
match user with
| Standard id -> $"Welcome, user id {id}."
| VIP name -> $"Glad to see you {name}, please enjoy your stay!"
greetingElement.textContent <- greeting
clickmeElement.onclick <- (fun mouseEvent ->
let user = if mouseEvent.altKey then VIP "Mr Bond" else Standard 123
greet user
)
Note some interesting things about this file:
-
#r "nuget: Fable.Browser.Dom"
This is a package download! It is a special feature of
.fsx
files - fetching dependencies straight from the script. They are great for experimenting with new ideas. -
The type
UserTier
with two cases:Standard
andVIP
, which is later fed into amatch
statement - this is an example of two much-beloved features of F#: discriminated unions and pattern matching. -
Functions such as
document.getElementById
will only accept the appropriate data type, so you won't be able to compile if you pass in the wrong type of value. For example in JS, you could calldocument.getElementById({hello: "world"})
and, despite the fact that an ID can't be an object, it will just silently return null at run time. In F# if you tried to do the equivalentdocument.getElementById {| hello = "world" |}
you would get a type error at compile time, helping you to catch the bug much earlier.
Last of all we need to run Fable in watch mode in a second console window (the first one is still running Vite).
dotnet fable watch app.fsx -o output
Output:
Fable 4.19.3: F# to JavaScript compiler Minimum @fable-org/fable-library-js version (when installed from npm): 1.4.2 Thanks to the contributor! @mexx Stand with Ukraine! https://standwithukraine.com.ua/ Parsing app.fsx... Project and references (2 source files) parsed in 1946ms Started Fable compilation... Compiled 2/2: app.fsx Fable compilation finished in 909ms Watching .
The result is that now we have an output
directory, containing both app.js
and another directory called fable-modules
. fable-modules
contains modules which recreate some F#-specific behaviours in JavaScript (see FAQs below).
Now check back in your browser (assuming you still have the Vite page open from earlier), and try clicking the button; then try again while holding the alt key.
Clicked normally...
With alt held down...
You see the result of the changes almost instantly:
- Changing the
.fsx
file caused fable to regenerate the.js
file (thanks to fable watch) - Changing the
.js
file caused Vite to do a hot reload
Summary
That's it for this installment of our series! We've seen how working in F# brings safety and syntactic elegance to the dangerous world of JavaScript and that adding Fable to our workflow is very lightweight.
Obviously the code in this blog post was just a simple example, but we can really do anything and everything in JavaScript using Fable.
Now that you have the foundation in place, stay tuned for the next chapter which introduces Feliz - an F# library for building applications in React.
Further questions
Q: Does working with Fable add a lot of complexity?
A: No, Fable is simple to set up and use.
Q: Why not just use TypeScript?
A: There are many reasons that we prefer F# to TS. To name a few: more type safety; discriminated unions and built-in pattern matching; the pipe operator (|>
); more collection functions; access to a lot of the .NET standard library in addition to the JS standard library.
Q: When new JS features come out, do I have to wait for Fable to be updated before I can use them?
A: No, you can use them immediately! Fable has "escape hatches" built in so you can always emit anything you need.
Q: I have too much existing code to switch in one go, can I switch over to Fable gradually?
A: Yes, you can import existing JS code very easily so you can start off by loading your existing JS from F# and then slowly migrate.
Q: Is there an official Fable site?
A: Yes, https://fable.io/.
Q: Are there other Fable browser wrappers? If so where can I find them?
A: Yes, there are lots over at https://fable.io/packages/.
Q: What are the "F#-specific behaviours" in the fable-modules
directory?
A: There are many; to name a few:
- Recreating built-in F# data types in JS, such as
Option
andResult
. - F# string interpolation is more advanced in F# with built-in type checking!
- .NET treats characters as a separate data type to strings - so some translation is required.
Q: Can I use Fable to transpile from F# into languages other than JavaScript?
A: Yes! To select other languages (currently in beta), use the --lang
option (JS is the default).