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:
- Generating JS from F#
- Writing React code from F# (this post)
- Using npm packages from F#
- 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:
-
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
-
Write your first module in Fable
We'll leave this step to you; or use the example above!
-
Compile your F# code
cd fsharp-modules dotnet fable
-
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#.
-
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.
-
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 ()
-
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.