United Kingdom: +44 (0)208 088 8978

Using F# to build React apps: components and hooks with Feliz

Did you know that you can use F# to build React apps? The second post in our series on that topic focuses on how to define React components in F#, and how you can mix-and-match JS/TS and F# during a gradual migration.

We're hiring Software Developers

Click here to find out more

Using F# to build React apps: Feliz

This is the second post in a series about how to build React apps using F#, covering a few topics:

  1. Generating JS from F#
  2. Writing React code from F# (this post)
  3. Using npm packages from F#
  4. Using the Elm architecture in your React components

This post focuses on Feliz, which provides a nice domain-specific language (DSL) for writing React components in F#. Let's get started!

The Feliz DSL

A key tool in writing React apps is JSX, the JavaScript extension that allows you to write DOM elements in JS. Even though you can still use JSX, Feliz provides a very convenient alternative DSL that allows you to write your DOM content in an F# native way.

Elements are created with a function taking a List of properties:

Html.a [
    prop.text "Coolest website ever!"
    prop.href "https://www.compositional-it.com/"
]

Or, in case of elements used to represent text, you can use an overload that takes just a string:

Html.p "Hello World!"

There is a special property, prop.children, that takes a list of child elements:

Html.ul [
    prop.children [
        Html.li "Falafel"
        Html.li "Hummus"
    ]
] 

But there's also an overload that takes children directly, for when you don't want to set any other properties:

Html.ul [
    Html.li "Falafel"
    Html.li "Hummus"   
] 

Because we're using a plain old F# List, we can also use list comprehensions or other List operations as we please. For example, the following JSX using sequence operations:

function list(items) {
   return (
           <ul>
              {items.map((item) => (
                      <li>{item}</li>
              ))}
           </ul>
   );
}

translates to:

let list (items: string list) =
    Html.ul [
        for item in items do
            Html.li item
    ] 

Writing React components

On to React itself! Feliz is a thin wrapper that allows us to write React components. Once we decorate our function with the [<ReactComponent>] attribute, we can start using React hooks to introduce state. Let's see a simple counter, using useState and useEffect:

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

    (React.useEffect 
         (fun _ -> printfn $"The count is now {count}"),
         [| box count |]
    )|> ignore

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

Migrating existing solutions

Writing React apps in F# is all fun and games, but what about the React apps you've already built? And your extensive React component library? Fear not! You can easily interoperate between "F# React" and "JS React" and vice versa.

I want to write components in F#

Adding F# components to an existing React app is not as difficult as it sounds:

  1. Set up an F# project inside your React project, installing Fable and Feliz:

    mkdir fsharp-modules
    cd fsharp-modules
    
    dotnet new -lang F#
    dotnet add package Fable.Core
    dotnet add package Feliz
    
    dotnet tool install fable --create-manifest-if-needed
  2. Write your first module in Fable

    We'll leave this step to you; or use the example above!

  3. Compile your F# code

    cd fsharp-modules
    dotnet fable
  4. Import and use F# modules in your React code

    import { Counter } from "@/fsharp-modules/Counter.fs.js"

Manually starting the compiler is cumbersome, so we recommend adding the Fable file watcher (dotnet fable watch) to your dev npm command (defined in package.json), and the compilation step (dotnet fable) to your build command.

I want to use JSX components in F#

Got the guts to start your new project in F#, but don't want to say goodbye to your existing React components? Feliz has your back, giving you the option to import JSX components into F#.

  1. Make sure your jsx modules are inside the project you want to call them from

    There are 2 main approaches here:

    • Either, create a new F# project that contains all modules you have available, and import that into the other F# projects that need them;
    • Or, import the jsx modules directly in the project that uses them.
  2. Write F# interop code

    Feliz Supports various ways to import jsx into F#, exposing the props as function arguments, a tuple or an (anonymous) record. To expose your jsx element with props as function arguments, simply define an F# function with the required signature, use React.Imported() as the function body and decorate the function with an attribute that points to the correct jsx module.

    [<ReactComponent(import = "Counter", from = "./Counter.jsx")>]
    let counter (initialState: int) (valueText: string) = React.imported ()
  3. Use your imported component just like any other ReactElement available in your Fable code

React hooks that are exposed in Fable and Feliz

Seasoned React devs might still be afraid of missing out on the multitude of hooks that come with React. Feliz has you covered with support for hooks such as UseDeferred, UseListener, UseMediaQuery and UseWorker.

Our favourite hook is probably a custom Feliz one, UseElmish. We'll take a deep dive into Elmish later in this series, but it provides a great way to maintain a data model, and takes some of the mutation logic out of the UI components. We use it to split out data models used on different site pages

Summary

As you can see, Feliz provides a great way to write React code with the power and safety of F#. When moving towards an F#-based approach there's no need to switch completely in one go. We presented a migration route that allow you to use jsx components in an F#-based React app, as well as an approach to implementing F#-based React components in a jsx app.