Can I guarantee `Cmd`s get sent after a view has been updated?

I have a view that has a list of items in a box with scrollbars. Currently, users can press arrow keys to select items in this list, so they can interact with them. When a user presses an arrow key

  1. My update function changes the selected item index in my Model
  2. It also issues a Cmd which checks if the selected-item is in view, and calls setViewportOf to scroll it.
  3. My view function has logic similar to
    if idx == selectedIdx then 
        Html.Attributes.id "selected-item"
    else
        Html.Attributes.classList []
    

This by and large works just fine, but what I’d like to know in the Elm Architecture is whether the cycle is guaranteed to be

update → (Model, Cmd) → Render Model → Issue Cmd

or whether it is possible for the Cmd to execute, scrolling the wrong element into view before the element with id="selected-item" gets updated.

I don’t think this is guaranteed, and furthermore I know it’s not guaranteed that the view is called after every update. Imagine if the user presses the arrow keys really fast, a bunch of updates will happen, but view may only be called once per frame.

For your case, I think the solution is to change how you use the html id attribute. It’s recommended practice that the id of a logical item shouldn’t change, so you can assign each item an immutable id and have your command scroll to the relevant id each time.

1 Like

I think it will be because Javascript in the browser is single threaded.

update → (Model, Cmd) → Render Model → Issue Cmd

Since the “Render Model” step is running and occupying that single thread, I don’t think events can be processed at all until it completes? Therefore, Cmds will not fire until view completes.

However, I am reasoning from first principles, not a detailed understanding of how the browser rendering cycle really works, so…?

1 Like

I think this is the direction I’ll end up going. Thanks! I’ve noticed that if the user holds the arrow key, it can get a bit wonky and the scrolling will get out of sync, but so far I’ve never seen that on a single press. I had assumed that was because the Cmds were firing out of order, but it could be due to rendering.

I think it depends on the command, but in practice every time I’ve seen such a question being asked, the answer was that Elm did the correct thing (and waited).

In this case, the underlying implementation of setViewport is this JS function

which uses _Browser_withWindow which uses requestAnimationFrame, meaning it will wait until the render is done (and therefore your element to have been inserted into the DOM)

Other Cmds, such as Browser.load AFAICT don’t necessarily wait for a re-render.

1 Like

Thanks for digging this up! This lines up with my previous understanding of the cycle, which is

Update › Issue Command › Optionally Update View

But this particular command has some logic that waits for the view to be updated before interacting with the dom.

In my reasoning above, I was thinking about Cmds that are resulting from events on the view. As per the flow given by the OP. So that cover the case where the user does a keypress or click on an item - those will happen only after the view is rendered, I think.

But in this case we are talking about a Cmd that is being initiated by the update. In other words not an event coming from the user through the UI, but one being programatically requested by the Elm code:

Update › Issue Command › Optionally Update View

These commands get run immediately I believe, right after update. Unless, as is the case with this particular Cmd, they are wrapped in the animation frame. Generally Cmds that interact with the view are run in the animation frame, and after the next view call completes.

It is also worth reading the notes here, about slightly different behaviours for event handlers on the DOM and how they are triggered, and the companion page about passive handlers:
https://package.elm-lang.org/packages/elm/virtual-dom/latest/VirtualDom#Handler

Could it be that your handler for the user pressing the arrow key is not passive? And that is why you see some lag effect?

There is some complexity here since the browser security model mandates that some actions (like for instance File.Select.file) have to be run in the same frame (i.e. synchronously) as the user event (like a button click) that initiated them, otherwise the browser will not permit these actions.

Hence Elm has to support running commands synchronously with processing events from the previous view.

On the other hand, there is no point in updating the view more than once per frame, since the user can literally never see those intermediate renders and since Elm is pure, neither can it have any effect on the state of the program.

Hence generally every update will execute Cmd and update the model, but a view render will be scheduled in the next animation frame. However, as has been pointed out, some commands naturally delay themselves to be also executed in the next animation frame, just after the render finishes (but not necessarily the same render, as theoretically the model may have been updated in between).

A pattern that can be used if you’d like to do the same sort of thing (we use this for some DOM-interacting ports for instance), you can use the following helper (which BTW I’m somewhat inclined to add to core-extra):

performLater : msg -> Cmd msg
performLater msg =
   Process.sleep 0
     |> Task.perform (always msg)
1 Like

I think you are mainly correct, but I think perhaps there can be cases where there may be a point in updating >1 per frame. Let me explain…

Lets suppose we have 2 side effects:

  1. The user clicks on an icon, generating a ClickedTheIcon event.
  2. The update upon recieving the ClickedTheIcon event, modifies the model so that a new part of the UI will appear at the bottom of the screen, and also requests a Cmd to scroll this new item into position.

If 1 does not cause 2 to happen immediately AND also update the view, when the Cmd to do the scrolling runs in the next animation frame it will not find the thing to scroll to? OR it will have to happen in the next+1 animation frame, in which case there will be a perceptable lag.

So I think a passive handler that runs right away and also generates the view is needed?

Again, hypothesising not going off what the actual code says, so I could be wrong…

I think if you have a command that doesn’t run immediately, but in the next animation frame (such as manipulating scroll), then there will always be a render that happens before.

However, if say a HTTP request completes before the next animation frame, then that may get processed before both the render and the scroll command are processed. The render (when it happens) than will be rendering the model that has the changes both from the ClickedTheIcon msg AND from the HTTPCompleted msg.

In that sense, the render from ClickedTheIcon was skipped.

So something like this:

… → update → process normal Cmds immediately → update → process normal Cmds immediately → … → cmd queue empty

on animation frame → render view → process delayed Cmds now

So in my example, the view would be rendered, but then right away in the same animation frame, the scroll would be started, and it would know where to scroll to since the DOM was built right before.

Makes sense, thanks!

1 Like

I would be somewhat opposed to including this in core-extra since it feels like an anti-pattern to me.

The reason this feels like an anti-pattern is that it muddles the concepts of command and message. It isn’t really performing some command later. Rather, it’s creating a command that delivers a message at some point in the future.

If I had a port that needed to interact with the DOM, I would use onAnimationFrame in the port callback. This would also remove the need for an additional message that does nothing but send a command to the port, which I think is involved in your setup based on that snippet.

Yes, please do add to core-extra! I have to do this sort of stuff from time to time.

@rupert,

In my reasoning above, I was thinking about Cmds that are resulting from events on the view. As per the flow given by the OP. So that cover the case where the user does a keypress or click on an item - those will happen only after the view is rendered, I think.

But in this case we are talking about a Cmd that is being initiated by the update. In other words not an event coming from the user through the UI, but one being programatically requested by the Elm code:

Update › Issue Command › Optionally Update View

These commands get run immediately I believe, right after update. Unless, as is the case with this particular Cmd, they are wrapped in the animation frame. Generally Cmds that interact with the view are run in the animation frame, and after the next view call completes.

I think your reasoning is basically correct, but although JavaScript is mostly single threaded, there is a “thing” about the event queue being able to “interrupt” the main thread, and also that the view function that updates the UI only runs if there are no pending Cmd’s (as in the last Cmd issued is a Cmd.none). Evan did a talk quite some time ago on this.

One can figure this out by reading the source code for the Elm scheduler in the Elm Core package or run a simple experiment to determine this. As Elm is just JavaScript with a much better and safer syntax, to allow the UI to update when there is lengthy “business” going on, one can resort to using Process.sleep just as one would use setTimeout in JavaScript in order to give the UI enough time to update…

I’ll post a couple of examples in a bit…

I don’t think that is correct. There is no preemptive threading in javascript. Only if the main thread give up control cooperatively, for example by waiting using await.

@rupert,

You are likely correct, and the event queue is just where events come from after the main thread is in a waiting state. At any rate, what I said about the normal event -> update -> view -> render cycle is not correct when the update returns a command other than Cmd.none as the runtime then immediate performs the (possibly chained) Task embedded in the Cmd which causes another update as in event -> update -> update -> ... -> view -> render as is proven by the following code:

module Main exposing (main)

import Browser exposing (element)
import Html exposing (Html, div, button, text)
import Html.Events exposing (onClick)
import Task exposing (perform, succeed)
import Process exposing (sleep)

type alias Model = Int

type Msg = Click | Finished

myUpdate : Msg -> Model -> ( Model, Cmd Msg )
myUpdate msg mdl =
  case msg of
    Click ->
      Debug.log ("Update Click " ++ String.fromInt mdl) <|
--      (mdl + 1, Cmd.batch [perform (\ _ -> Finished) <| succeed (), Cmd.none])
      (mdl + 1, Cmd.batch [perform (\ _ -> Finished) <| sleep 1000, Cmd.none])
    Finished ->
      Debug.log ("Update Finished " ++ String.fromInt mdl) <|
      (mdl + 1, Cmd.none)

myView : Model -> Html Msg
myView mdl =
  Debug.log ("View " ++ String.fromInt mdl) <|
  div []
    [ button [onClick Click] [text "Click me..."]
    , String.fromInt mdl |> text
    ]

main : Program () Model Msg
main =
  element
    { init = \ _ -> ( 0, Cmd.none )
    , update = myUpdate
    , view = myView
    , subscriptions = \ _ -> Sub.none
    }

which returns the following as logged output for two clicks on the button:

View 0: 
Update Click 0: (1,)
Update Finished 1: (2,)
compile:341 View 2: 
Update Click 2: (3,)
Update Finished 3: (4,)
View 4: 

Note that there is no update of the view between the updates. If the succeed () is changed to sleep 1000 by changing the non-commented active line, the output is as follows:

View 0: 
Update Click 0: (1,)
View 1: 
Update Finished 1: (2,)
View 2: 
Update Click 2: (3,)
View 3: 
Update Finished 3: (4,)
View 4: 

which shows that using the sleep to yield the main thread allows a Cmd.none to be returned and thus a view update (and render, since the contents of the VirtualDOM change) with the rest of the dual command completing after the time delay (a second in this case, but could be much shorter allowing just enough time to complete the view update and render…

This is the way one has to allow for UI updates such as progress indications and active Cancel buttons when executing long running jobs: break them up to update the UI periodically, and restart them after a sufficient time delay for the UI to render.

So in answer to the OP, whether the usual cycle is followed is dependent on what your Cmd is, if it performs a task that depends on the UI updating then it won’t work as the UI isn’t updated until the update outputs a Cmd.none. In the above case, Cmd.none is output at the end of the Cmd.batch but this only happens immediately if the commands in the batch list contain a sleep…

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.