Since starting with Elm I’ve had a vague notion that it is prudent to keep the types of your messages slim (not containing new state).
I think this notion came from a relatively early bug that I had, but now I’m questioning it and wondering if anyone has any insight.
Let me provide an example of something that comes up quite a bit. Suppose we have a model consisting of some kind of ‘item’ which we want to be able to update. Items themselves already contain some kind of ID, it may be some kind of server-side database ID that is decoded when the original set of items is downloaded.
type alias ItemId = String
type alias Item =
{ id: ItemId
, someValue : Int
, someOther : String
}
type alias Model {
items : Dict ItemId Item
}
Now suppose I have some kind of form for updating an item. What should that message have as its argument, the entire Item
or just the ItemId
with a function to update the stored Item
?
type Msg
= UpdateItem ItemId (Item -> Item)
update msg model =
case msg of
UpdateItem itemId updateFun ->
case Dict.get itemId model.items of
Nothing ->
-- Hmm this is strange, silently fail for now
( model, Cmd.none)
Just item ->
let
newItems =
Dict.insert itemId (udpateFun item) model.items
in
( { model | items = newItems }
, Cmd.none
)
Or we could just store the new item on the message:
type Msg
= UpdateItem Item
update msg model =
case msg of
UpdateItem item ->
let
newItems =
Dict.insert item.id item model.items
in
( { model | items = newItems }
, Cmd.none
)
At some point I got bitten by autocomplete. I had a registration form and I had an UpdateRegisterForm
message which had the new RegisterForm
as the argument. When parts of the form were filled out by autocomplete the problem was that several of the onInput
handlers were fired without a render in-between. This meant that the final update essentially overwrote all of the previous updates. The solution was that the UpdateRegisterForm
message took as argument a function to update the current RegisterForm
rather than a whole new one. I think this experience edged me towards messages carrying functions to update the current state, rather than carrying new state within them. So I guess I am reconsidering that reasoning and wondering whether the extra complexity is worth it.
The other approach is to have a different message for every kind of update to an item that is possible. Something like:
type Msg
= UpdateItemSomeValue ItemId Int
| UpdateItemSomeOther ItemId String
I think this is conceptually equivalent to the first approach with UpdateItem ItemId (Item -> Item)
except that the function is implicit/defined in update
. I think with the function approach you may have many fewer messages which can reduce noise quite a bit, at the expense of making the input handlers more complicated.
Any thoughts?