United Kingdom: +44 (0)208 088 8978

Using F# to build React apps: state management with Elmish

Did you know that you can use F# to build React apps? The fourth post in our series on that topic focuses on using the Elm architecture for managing your React component state

We're hiring Software Developers

Click here to find out more

Using F# to build React apps: Elmish

This is the fourth 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#
  3. Using npm packages from F#
  4. Using the Elm architecture in your React components (this post)

In this post, we'll discuss the approaches to state management popularised by Elm programmers, why you might want to use them, and how to use them from F#. Let's jump right in!

Elmish

The Elm architecture has become a popular pattern for building functional web applications. While originally developed for the Elm language, its principles have been adopted in other ecosystems, including F# through the Elmish library. State management within a React app that's written in F# can be approached in two different ways: using the ubiquitous hooks that the React library provides, or using The Elm Architecture through Elmish. Both approaches are valid and you can read more on the former. However, In this post, we'll explore how Elmish brings the benefits of the Elm architecture to F# and React applications, and how it compares to patterns you may be familiar with from React.

The Elm Architecture

The Elm architecture is a simple yet powerful pattern for organizing code in functional web applications. It consists of three main parts:

  1. Model: Represents the state of your application
  2. Update: A function that updates the state based on messages
  3. View: A function that renders the UI based on the current state

The Elm Architecture
Source: https://github.com/huytd/everyday

Interestingly, this architecture wasn't explicitly designed by Elm's creator. Rather, it emerged naturally from the constraints of functional programming and was later standardized as developers converged on similar patterns.

From React to Elmish

If you're familiar with React, you might see some similarities to its state management approaches. Let's start with a simple example using React's useState hook:

function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Now, let's see how we can achieve the same functionality using Elmish with the useElmish hook in F#:

let init() = 0, Cmd.none // Tuple of (Model, Command)

let update msg model =
    match msg with
    | Increment -> model + 1, Cmd.none

[<ReactComponent>]
let Counter() =
    let model, dispatch = React.useElmish(init, update)
    Html.div [
        Html.p [
            prop.text $"Count: {model}"
        ]
        Html.button [
            prop.text "Increment"
            prop.onClick (fun _ -> dispatch Increment)
        ]
    ]

Let's break down further what is happening here.

As you can see, Elmish introduces a more structured approach to state management. The init function sets up the initial state of your model, while the update function handles state changes based on dispatched messages.

The dispatch function might feel similar to setState function you're familiar with. However, there's a key difference: instead of directly setting the new state, dispatch sends a message describing what change should occur with any associated data attached to that message if need be. It is then in the update function where the state/model is directly recreated with any new value(s).

Think of it this way:

  • React: setCount(count + 1)
  • Elmish: dispatch Increment

The dispatch function sends messages to the update function, which then decides how to change the state based on the message received. This is similar to how you might use useReducer in React, where you dispatch actions to a reducer function.

Diagram showing Elmish MVU loop

This separation of concerns - where UI events dispatch messages and a separate update function handles those messages to produce new state - is a core principle of the Elm architecture. It helps keep our code predictable and easy to reason about, as all state changes are centralized in the update function.

Handling Side Effects

While you can use it to replace useState, the benefits of useElmish become more apparent in components with side effects.

For more complex state management in React, developers often turn to useReducer.

Elmish provides a similar separation of concerns to useReducer, but with a more functional approach and built-in support for handling side effects explicitly via the Cmd module. By contrast, with useReducer you have to manage side effects separately.

In React, side effects are typically managed using the useEffect hook. Here's a common pattern for handling API calls:

function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    setIsLoading(true);
    // `fetchUser` is an omitted function that takes in a user ID and asynchronously returns a `User`.
    fetchUser(userId)
      .then(data => setUser(data))
      .catch(err => setError(err))
      .finally(() => setIsLoading(false));
  }, [userId]);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!user) return null;

  return <div>{user.name}</div>;
}

In this case we are fetching some user information for a user of a given ID through an asynchronous operation. With Elmish, we can handle this more elegantly using a set of discriminated unions which document the journey of fetching user data.

type Model =
    { User: User option
      IsLoading: bool
      Error: string option }

type Msg =
    | LoadUser of int
    | UserLoaded of User
    | LoadFailed of string

let init userId =
    { User = None; IsLoading = false; Error = None },
    Cmd.ofMsg (LoadUser userId) // Initially load the user once the component mounts

let update msg model =
    match msg with
    | LoadUser userId ->
        { model with IsLoading = true; Error = None },
        // `fetchUser` is an omitted function that takes in a user ID and asynchronously returns a `User`.
        Cmd.OfAsync.either fetchUser userId UserLoaded (fun ex -> LoadFailed ex.Message)
    | UserLoaded user ->
        { model with User = Some user; IsLoading = false }, Cmd.none
    | LoadFailed error ->
        { model with Error = Some error; IsLoading = false }, Cmd.none

[<ReactComponent>]
let UserProfile userId =
    let model, dispatch = React.useElmish(init userId, update)

    match model with
    | { IsLoading = true } -> Html.p "Loading..."
    | { Error = Some error } -> Html.p $"Error: {error}"
    | { User = Some user } -> Html.div user.Name
    | _ -> Html.none

This approach keeps our state management logic centralized and makes it easier to reason about the different states our application can be in.

Summary

Elmish brings the power and simplicity of the Elm architecture to F# and React applications. By providing a structured approach to state management and side effects, it allows us to write more maintainable and predictable code. While it may require a shift in thinking for developers accustomed to traditional React patterns, the benefits in terms of code organization and reasoning about application state make it a valuable tool for writing user interface code.