When does elm (not) update the UI?

Hi there,

Making my first pleasant steps into elm building a prototype app that does pattern matching on a fairly large data structure loaded from an external source. My question is quite basic, I can imagine a referral to some doc/blog post could do.

The pattern matching is side-effect free and can take from 1 to say 30 seconds. It’s triggered by an onClick event. This being a prototype, it’s okay for the UI to ‘freeze’ during these calculations (a production version might offload the calculation to a web worker or server-side implementation).

But I would like the UI to show that it’s busy to the user. For this I have a boolean property busy in my model. The UI is rendered accordingly.

What I’ve not succeeded in is getting the UI to be updated before (busy) and after (not busy) the calculation. I consulted a.o. “How to turn a Msg into a Cmd Msg in Elm?”. My current implementation (ab)uses a sequence of commands (ExecuteScenarioClicked, ExecuteScenario, ScenarioExecuted) during which the busy status is set True, the work is performed and the busy status is set False.

This does lead to the UI updates I expect when stepping through the debugger. But when running live no update is shown and the UI shows the non-busy status (even during 30 seconds).

So my primary question is when elm decides to rerender parts of the UI. A secondary question is what a suitable pattern would be for achieving an ‘update ui’ - ‘do stuff without side-effects’ - ‘update ui’ cycle would be.

Thanks for any pointers!


It would be interesting to see some code.

For example are you doing something like this:

update msg model =
   case msg of
      OnClick -> ({ model | busy = True, result = longCalc model }, Cmd.none)

Obviously in that case, the model with busy = True is not going to be produced until after the longCalc function completes.

Or are you doing like this:

message : msg -> Cmd msg
message x =
   Task.perform identity (Task.succeed x)

update msg model =
   case msg of
      OnClick -> ({ model | busy = True }, message DoLongCalc)

      DoLongCalc -> ({ model | result = longCalc model }, message Done)

      Done -> ({ model | busy = False }, Cmd.none)

Which may fair better. At least it is returning control to the Elm runtime with busy = True, to update the view. However, the long running calculation can still block the UI thread and prevent the UI from being updated.

Elm renders the view during a Window.requestAnimationFrame(). So if the busy = True does not get picked up by the next animation frame, and the long running calculation is already started, I think the UI will get frozen out until the calculation completes.

The way I worked around this, is to split my calculation down into steps, and return a continuation for the next step at each step until the calculation is complete. Like this:

type CalcResult a = 
     Complete a
    | Ongoing a (() -> Result a)

longCalc : Arg1 -> ... -> Argn -> SomeResultType -> CalcResult SomeResultType

So once you supply longCalc with its arg, you get a step function to go from the last intermediate result to the next one, which may be another intermediate result, or the final result of the calculation.

Then you can run a number of steps (maybe 100), in a single iteration of the update, then return control to Elm to render the UI, then calculate some more.

It works, but I found that generally doing too much calculation in the update loop will very negatively affect the responsiveness of the UI. It is also explicit application controller time-slicing which is not so nice to have to do - at least not in a high level language, you might expect to have to do stuff like that on a micro-controller or something.

Thanks for your extensive reply. The logic I use matches your 2nd example, so the runtime is involved. I figured this would ensure updates to become visible but from your answer I understand that there’s a time window involved here as well.
The slicing you suggest will work for me at the expense of somewhat less clean code. So I think I’ll go down that route.
(I’m tempted to start playing with web workers but as I may want to pass functions later on, that won’t fly).

If you aren’t able to break the calculation into steps, you can insert a delay before the calculation begins. This gives Elm a chance to render the UI. Here’s an approximation of the code I’m using in my app. I suspect 100 ms is much greater than necessary, but I haven’t yet tried tuning it.

message : msg -> Cmd msg
message =
    Task.perform identity << Task.succeed

delay : Time -> Float -> msg -> Cmd msg
delay time unit msg =
    Process.sleep (time * unit)
        |> Task.andThen (always <| Task.succeed msg)
        |> Task.perform identity

update msg model =
    case msg of
        OnClick ->
            ( { model | busy = True }, delay 100 Time.millisecond DoLongCalc )

        DoLongCalc ->
            ( { model | result = longCalc model }, message Done )

        Done ->
            ( { model | busy = False }, Cmd.none )
1 Like

@menno Elm uses requestAnimationFrame to avoid re-rendering any faster than 60 times a second. (The eye couldn’t see anything faster so it would be a waste).

This rendering timing is driven by the browser. But messages and updates are not on any such schedule, they just happen whenever they happen.

So when you kick off your long calculation it won’t wait for a render, it’ll just happen straight away. It might be worth using a delay >16ms with Process.sleep.

I’m kind of in the same position as you on that. It means you cannot have a general mechanism whereby you can pass functions to a background worker. However, the background worker can have the function already in its code, and you pass it non-function arguments, and I think you could cover most use cases this way. That is what I plan to do anyway.

Also note that the intermediate CalcResult Ongoing cannot be passed back from the worker thread, because it contains the continuation function:

type CalcResult a = 
     Complete a
    | Ongoing a (() -> Result a)

So the worker thread needs to keep that in its local state, and pass back something that can be serialized into a json Value. Presumably the value of whatever you substitute for the type variable a in the above type.

I am not even sure that javascript can pass a continuation to a web worker? Would be interesting if someone could shed some light on that; I think this places some limitations on what support for multi-threading Elm could ever implement.

The delay indeed solves my issue of updating the UI before starting the long calculation. Thanks, also to @Brian_Carroll! In my case 20ms seems enough.

I ended up using this shorter construct I found on Stack Overflow:

sendDelayed : (a -> msg) -> a -> Cmd msg
sendDelayed msg a =
    Process.sleep (20 * millisecond)
        |> Task.perform (\_ -> msg a)

but to be honest my Elm knowledge is too limited to judge this against the Task.succeed etc. approach I’ve seen more often.

Great, that code looks good!

If you replaced Process.sleep in your code with Task.succeed () then it would do the same thing without the delay, like this:

sendNotDelayed : (a -> msg) -> a -> Cmd msg
sendNotDelayed msg a =
    Task.succeed ()
        |> Task.perform (\_ -> msg a)

Here we use Task.succeed to wrap the empty tuple in a Task type. The lambda function then accepts that empty tuple, ignores it, and generates a message.

It’s a slightly more roundabout way of doing things compared to the code below (which may be what you’ve seen before?)

sendNotDelayed2 : (a -> msg) -> a -> Cmd msg
sendNotDelayed2 msg a =
    Task.succeed a
        |> Task.perform msg

So perhaps it’s the use of the empty tuple that’s new to you? This is an idiomatic way of doing lazy execution in Elm. In other languages you’d use a function with no arguments and you wouldn’t need the empty tuple. But that concept doesn’t quite work in Elm, so you do this instead!

(Laziness with the empty tuple also comes up a lot in elm-test, by the way.)