United Kingdom: +44 (0)208 088 8978

Using F# to build React apps: transpiling to JS with Fable

Did you know that you can use F# to build React apps? The first post in our series on that topic focuses on what's to like about F# and how you can use F# to write code that runs in the browser.

We're hiring Software Developers

Click here to find out more

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:

  1. Generating JS from F# (this post)
  2. Writing React code from F#
  3. Using npm packages from F#
  4. 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:
  • 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/

Web browser showing the text "Hello." and a button labelled "click me!"

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 and VIP, which is later fed into a match 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 call document.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 equivalent document.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...
Web browser showing page containing the message "Welcome, user id 123."

With alt held down...
Web browser showing page containing the message "Glad to see you Mr Bond, please enjoy your stay!"

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 and Result.
  • 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).