United Kingdom: +44 (0)208 088 8978

Eliminating infinite loops in nested Elmish

When dealing with a polling loop in an Elmish application, Joost ran into some curious behaviour; in this blog post, he explains a few solutions to his problem of an infinite loop!

We're hiring Software Developers

Click here to find out more

If you've used the SAFE stack, you'll be familiar with Elmish. If not, give it a go! It's a fantastic way of managing state in F# apps. React.UseElmish is a custom React hook that allows you to create Elmish components wherever you like in your React app. We commonly use it to split applications out in separate Elmish loops. Another approach to this is composing multiple Elmish hooks together.

Both of these approaches have their advantages, and there is no right way of doing it; I tend to reach for UseElmish, because it has very little boilerplate; composition, on the other hand, provides a more native way for the parent loop to respond to events in the child loop. In this blog post, I'll explain a pitfall when using loops in UseElmish, and how you can fix it.

If you have to do something in a loop in Elmish, it's quite easy to achieve that behavior by writing a message that returns itself when handling. Take this example of a counter that increments every second:

type Model = { counter: int }

type Msg =
    | Loop
    | PollAction

let init _ =
    { counter = 0 }, Cmd.ofMsg Loop

let update sendMessage msg model =

    match msg with
    | PollAction ->
        {
            model with
                counter = model.counter + 1
        },
        Cmd.none

    | Loop ->
        model,
        Cmd.batch [
            Cmd.ofMsg PollAction
            Cmd.OfAsync.perform (fun _ -> Async.Sleep 1000) () (fun _ -> Loop)
        ]

Upon initialization, it starts the loop. On every Loop message, two messages are dispatched: one that updates the counter, and another that will wait for a second, to then produce a new message that kicks off the loop again.

I recently wrote some code like this, and it did exactly what I expected... until someone pointed out that it keeps running after they left the page it was on! What's even worse, every time you'd visit the page, the polling would double in frequency.

I wrote a little demo of the problem; on the page there are three different implementations of a component that sends messages to a shared list; one broken implementation, and two with different fixes. Notice how even after you close the broken useElmish component, it keeps generating messages!

Using composition

After a little investigation, I rewrote the UseElmish component that was causing the issue as a child of the parent loop that manages the state for the full website. This works well, as the parent loop will stop dispatching messages to its children once you've navigated away from the page:

let update msg model = 
    match msg with
    | ComposedMessage message ->
        let outerCmd =
            match message with
            | Composed.Msg.PollAction -> Println "the inner message is polling!"
            | _ -> Cmd.none

        let page, cmd =
            match model.Page with
            | Page.Composed model ->
                let updatedMessage, newCommand = Composed.update message model

                Page.Composed updatedMessage, Cmd.map ComposedMessage newCommand
            // ignore the message if we're not on the right page!
            | other -> other, Cmd.none

        { model with Page = page }, Cmd.batch [ outerCmd; cmd ]

Note that we match on message type AND the current page, and ignore the message if they don't match up.

That was the customer's problem solved, and I moved on to finding a definitive solution for this UseElmish problem... To finally read the docs, and realize the answer was there all along!

Implementing IDisposable

When using useElmish, you can implement IDisposable on the model. Once the React element that uses the hook is removed from the page Dispose is called, allowing you to break the cycle. Since Dispose has a C#-esque signature of T -> (), we'll have to add some mutability to the mix:

type Model = {
    counter: int
    mutable disposed: bool
} with

    interface System.IDisposable with
        member this.Dispose() = this.disposed <- true

    member this.looping = not this.disposed

type Msg =
    | Loop
    | PollAction

let init _ =
    { counter = 0; disposed = false }, Cmd.ofMsg Loop

let update sendMessage msg model =
    match msg with
    | PollAction ->
        {
            model with
                counter = model.counter + 1
        },
        Cmd.none

    | Loop ->

        let nextPoll =
            match model.looping with
            | true -> Cmd.none
            | false -> Cmd.OfAsync.perform (fun _ -> Async.Sleep 1000) () (fun _ -> Loop)

        model, Cmd.batch [ nextPoll; Cmd.ofMsg PollAction ]

We added a boolean to keep track of the disposal of the model, and added an attribute that indicates whether the app should keep the loop going. We now have a nice, clear terminating condition for our loop! As you have already seen in the demo app, this version will end as soon as you leave the page.

As you can see, when using UseElmish, if your Elmish loop is still receiving messages, it will run on forever. This will cost resources, but can also lead to unintended side effects! If you are using UseElmish inside of another Elmish loop, you can fix it by making it a child loop of the outer Elmish loop, although simply implementing IDisposable is probably the easier way to do it. In the end, I was happy to have migrated to a nested Elmish configuration, as it made dependency management of the two loops a bit easier.