That is a good example of tricky concurrency that is not obvious at first.
I would like to elaborate on some things
- The
view
and the DOM is always “behind” the Model. It will update at most every animation frame (commonly every 16ms but might be slower) and always run after the call toupdate
- Multiple messages might be processed (batched) before the
view
- The order in which events from the DOM are received/processed is difficult to reason about and might vary across browsers (I don’t have any evidence for this, it’s just an assumption).
- The unpredictability of message ordering also applies to some commands (Cmd), for example Http requests. Here is an example, do you see the bug or the problem?
type Msg
= SearchInputChanged String
| GotSearchSuggestions (Result Http.Error (List String))
update msg model =
case msg of
SearchInputChanged newText ->
( { model | searchText = newText }
, fetchSuggestionsFromServer newText
)
GotSearchSuggestions (Ok suggestions) ->
( { model | suggestions = suggestions }
, Cmd.none
)
GotSearchSuggestions (Error_) ->
( model
, Cmd.none
)
fetchSuggestionsFromServer text =
Http.get
{ url = "http://example.com/?search=" ++ text
, expect = Http.expectJson GotSearchSuggestions (Json.Decode.list Json.Decode.string)
}
The problem is the assumption that the response from the server will come in the order requests where sent. If two requests are sent and the first takes longer for the server to process the responses will arrive in reversed order. This will result in model.suggestions
containing stale data.
A solution here is to include the search text in the reply msg by adding an argument to GotSearchSuggestions
:
type Msg
= SearchInputChanged String
| GotSearchSuggestions String (Result Http.Error (List String))
Now you can check if the suggestions you received match model.searchText
and if not, throw the result away.