United Kingdom: +44 (0)208 088 8978

Which React hooks to use from F#

Confused about how to choose between useState, useEffect and useElmish in your F# React code? In today's post, Matt provides some guidelines.

We're hiring Software Developers

Click here to find out more

When you make React components using Feliz, there are a few different hooks that you can use. One extra complication is that the React.useElmish hook provided by the Feliz.UseElmish package can be used for state management and for handling asynchronous actions, such as communication with an API. Seasoned React devs will notice that there's some overlap with the useState and useEffect hooks. So how should you combine the three?

There's no objective answer, but there also don't seem to be loads of opinions available on the web. I thought I'd share my opinions in case they're helpful to clarify others' thoughts.

useEffect versus useElmish

useEffect versus useElmish is straightforward: my advice is to always favour useElmish. I find that it significantly clarifies code that triggers asynchronous processes and reacts to their results. Of course, if you have use cases for useEffect where you can't use useElmish, then go ahead and use useEffect; in practice I haven't encountered any while developing SAFE Stack apps.

useState versus useElmish

useState versus useElmish is a bit more subtle. Managing state with useElmish incurs some overhead compared to managing it with useState: rather than just calling a set function, we instead dispatch a set message that is handled by an update function, which updates a model appropriately. It's not particularly taxing to write or understand when you're fluent with Elmish, but it is overhead nonetheless.

open Feliz

module UseState =

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

        React.fragment [
            Html.p count
            Html.button [ prop.text "Increment"; prop.onClick (fun _ -> setCount (count + 1)) ]
        ]

module UseElmish =

    open Elmish
    open Feliz.UseElmish

    type Model = { Count: int }

    type Msg = Increment

    let init () = { Count = 0 }, Cmd.none

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

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

        React.fragment [
            Html.p model.Count
            Html.button [ prop.text "Increment"; prop.onClick (fun _ -> dispatch Increment) ]
        ]

The question we need to ask is when are the benefits of storing state via useElmish worth the price of the overhead? To keep things simple for the rest of this post, I'm going to make the assumption that we're talking about a component without child components managing their own state that they occasionally push to their parent. Obviously, that's not always the case, but it at least gives us a good base to start from. We'll cover the more complex case with child components in a future post.

In the simpler case without child components, the way I see it is that the key factor is whether state comes from the server or affects what goes to it. Let's explore that in more depth.

Data sent to or received from the server

As already mentioned, useElmish is great for managing asynchronous interaction with the server. What about managing state that affects those interactions? For data received from the server and handled in the Elmish update function, it's easier to add it to the Elmish model than it is to get it into useState state, so it's a no-brainer to store it in the Elmish model.

For data that is sent to the server there are two options to choose between:

  1. Manage it in the Elmish model

        let update msg model =
            match msg with
            | UpdateMessageForServer message ->
                {
                    model with
                        MessageForServer = message
                },
                Cmd.none
            | ContactServer -> model, Cmd.OfAsync.perform api.Endpoint model.MessageForServer HandleResponse
            ...
        ...
        []
        let comp () =
            ...
            React.fragment [
                Html.input [ ...; prop.onChange (dispatch << UpdateMessageForServer) ]
                Html.button [ ...; prop.onClick (fun _ -> dispatch ContactServer) ]
                ...
            ]
  2. Manage it via useState and dispatch it in the Elmish message

        let update msg model =
            match msg with
            | ContactServer message -> model, Cmd.OfAsync.perform api.Endpoint message HandleResponse
            ...
        ...
        []
        let comp () =
            ...
            React.fragment [
                Html.input [ ...; prop.onChange setMessageForServer ]
                Html.button [ ...; prop.onClick (fun _ -> dispatch (ContactServer messageForServer)) ]
                ...
            ]

When the data is only used in one type of API call, it's debatable and I don't see a strong case either way. However, when the data is used in multiple types of API calls then I think keeping it on the Elmish model is simpler. There might be more small Elmish messages for reacting to state changes, but they're all simple and there's no data wiring-up required for the messages that trigger the API calls. To sum up, there's a weak case for useState in the simplest case and there's a strong case for useElmish in the more complex cases.

Given that applications have a tendency to become more complex over time as more features are added, I feel comfortable recommending that state that is sent to the server should always be managed via useElmish. That includes the cases where a derivative of the state is sent to the server.

So model state that is either data directly sent to or received from the server, or is a derivative of such data should be managed by useElmish. What about other state? Should that live on the Elmish model or be managed by the useState hook?

Client-side only data

Client-side only data is data that's only used to affect the presentation of the UI. It might represent the tab that a user currently has visible on a tabbed component, or whether a modal dialog is currently open. To me, that's perfect state to manage via useState: it's not going to the server, so you're not getting benefits from it being managed via Elmish; therefore, don't pay the price (even though it's small)!

Summary

We've come to some simple guidelines:

  • Always prefer useElmish to useEffect for asynchronous interactions.
  • If data in a component without children is sent to or received from the server, manage it via useElmish; if it is only used by the client, manage it via useState.

Having used this post to clarify my thoughts, I feel comfortable recommending that approach to you, dear unknown reader on the web. That said, if you find cases where you think this approach doesn't work well, we'd love to hear them!

The situation for components with child components managing their own state is more complex, and deserves its own post, which we'll publish in the near future. In the meantime, I hope that this post makes choosing hooks when developing Feliz applications that little bit easier. Happy coding!