Message types carrying new state

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?

1 Like

I usually try to keep data from the backend separate from form states. Then I use a helper function to merge data with form state to pass it to the render function.

Right, I usually do that as well, and then I have an easy way to check if there are unsaved changes.
I was trying to keep the example as minimal as possible.

I’ve used the approach with a single Msg containing a function, too. My goal was to reduce the amount of messages and update cases for a form.

The form is modeled with a record (also containing a dynamically large list of subform elements) and I use elm-monocle lenses and optionals to specify which field is changed and a function to modify the value (mostly the value get’s replaced). Each field will fire the same message on change. The approach worked well for me and was easy to extend (e.g. validation, new fields).

2 Likes

Here’s how I’ve come to think of it:

  1. A message’s job is to carry data to update. I think it’s generally a mistake to put a function in a message, both from a general design perspective as well as because it means the message won’t work in the debugger anymore.
  2. A good guideline I’ve found for message design is to have each message answer the question “what happened?” rather than “what should update do?” I’ve started naming messages accordingly, e.g. onInput EnteredEmail rather than onInput SetEmail, because “what happened” is that the user entered an email address. It’s up to update what to do based on what happened.
  3. Sometimes I’ve found it useful for the message to hold data not just about “what happened?” but also “under what circumstances?” For example, let’s say there’s a Maybe Credentials in the model because the user may or may not be logged in. If I’m rendering a Favorite button that only gets rendered when the user is logged in (meaning the model holds a Just Credentials), if update will need those Credentials to send the HTTP request that records the Favorite, I’ll store the Credentials in the ClickedFavorite message that gets sent from view. This way, inside update I don’t need to do a case on the Maybe Credentials and have a “this should never happen” branch. I used this technique in elm-spa-example.

That said, there are potential pitfalls to be aware of when doing (3). Storing information in a message that will be returned by a Cmd, for example, can definitely cause problems if the information becomes stale between when the Cmd is fired and when it ultimately delivers its message—potentially several seconds later.

I’m guessing the autocomplete bug you encountered was on a release of Elm prior to 0.19, because onInput now synchronously triggers a re-render each time it fires (precisely because of synchronization issues like this), so I don’t think the bug you encountered would still be reproducible on 0.19. I could be wrong though!

Still, even if it did not cause a race condition, I wouldn’t choose to store the whole Item in the message, because that would mean view would be eagerly updating each Item just to set up the handler. That’s unlikely to cause a performance problem in practice, but it does mean view is doing a lot more than setting up a “here’s what happened” message. I would rather have all the logic for “what to do based on what happened” live in update, even if it means update has to handle the “what that ID is not in the collection” case that I expect never to happen.

26 Likes

Thanks that’s a great reply. I am pretty sure that the function-in-a-message was entirely based on the auto-complete bug, though it does also mean that you only need one message. I know that you can still have a single message if you just have a further custom type for what happened, and thinking about that, that would solve the auto-complete bug even if 0.19 doesn’t solve it anyway.

I was also aware that having a message returned from a Cmd that might take several seconds risks having stale data in the message that you then use to update the current data. I think that has contributed to my general feeling to keep the data in a message slim.

Anyway thanks for the perspective, I like the idea of thinking a message should state what has happened not what should be updated.

1 Like

Hey, agree with what Richard said in term of concept: in my mind, a Msg is for sending a message of thing that a user did. It’s up to the update function to decide to do with that data, e.g. potentially make a state change based on the new data. I personally would never put a function inside the Msg and am not sure what it would buy you in the long run.

We have tons of forms, use a single update function, and use auto-complete heavily. Works like a charm. Not sure why you are having a “bug”, maybe the code below will help out???

In term of how it works, we (basically) have a single update function where we pass a dot identifier (to know what “field” we are updating) and the value. This is stored in a Dict.

E.g. something like:

(I tried to provide enough details to explain the concept–this is a bit simplified from what we actually do, e.g. we push to a DB, have form validation, convert the dot notation to JSON downstream, have a function for every form field type, etc.)

We tried a bunch of different methods before doing it this way, but this general approach has worked well.

-- view function
viewForm : Model -> Html Msg
viewForm =
    div []
        [ label [] [ text "Mobile Phone" ]
        , textInput "applicants.1.contact.phoneNumbers.1.phoneNumber" "646-555-1212" "tel" model.formAnswers
        ]

textInput : String -> String -> String -> Dict.Dict String String -> Html Msg
textInput formId placeholder autocomplete formDict =
    input
          [ id formId
          , attribute "autocomplete" autocomplete
          , name formId
          , placeholder placeholder
          , value <| getFormAnswer formId formDict
          , onInput (\answerValue -> SetFormAnswerValue formId answerValue)
          , type_ "text"
          ]
          []

getFormAnswer : String -> Dict.Dict String String -> String
getFormAnswer formId formDict =
    case Dict.get formId formDict of
        Just answerValue ->
            answerValue

        Nothing ->
            ""

-- Model:
type alias Model =
    { formAnswers : Dict String String }

type Msg
    = SetFormAnswerValue String String

-- update function:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SetFormAnswerValue formId answerValue ->
            let
                updatedFormDict =
                    Dict.insert formId answerValue model.formAnswers
            in
            ( { model | formAnswers = updatedFormDict }, Cmd.none )

1 Like

I’m of two minds about this. For some messages, this makes a lot of sense: Msgs.SearchFormSubmitted, but in other places, I rather use a structure like Msgs.NavigateTo new_route, because otherwise I’ll have a message for each link or button in the application. Msgs.BackToHomepageButtonClicked does not seem that useful.

But it is a bit difficult for me to see where the divide between these two strategies should be. Does anyone have any tips?

3 Likes

How does update work for SetFormAnswerValue? Does this mean you’re storing the form as a dictionary? In which case the update is just a Dict.insert. That kind of means you’re losing out on some type-safety guarantees (in particular if you update the name of a field you have to manually find anywhere it is used). That’s what the function in the method ‘buys us’, if you store the form as a record you cannot update it in the same way be giving the name of the field and the new value (well in theory you could case on the name of the field but you would be fighting the type system rather than utilising it).

Yup. We do give up a bit of Elm type safety by not using a custom type on a per-field basis.

Three things, though.

  1. All html form input field are actually type String, regardless of what they look like. They must be convert to and from other types as needed.
  2. We have a custom built form validation library that checks each field to ensure that the type is correct. It is much more sophisticated than the type checking like Elm would do, e.g. sometimes fields can only be certain values if other fields have been previously filled in a certain way, a SS number can only have a certain format, a phone number must look a certain way, etc. In other words, form validation also involves dependency checking for us as well as type checking.
  3. Our form Dict is actual Dict String AnswerValue, not Dict String String as I showed in the simplified version above. AnswerValue is a custom type that looks like this:
type AnswerValue
    = AnswerValueNone
    | AnswerValueString String
    | AnswerValueBool Bool
    | AnswerValueCsv String
    | AnswerValueFloat Float
    | AnswerValueInt Int
    | AnswerValueRangeInt ( String, Int, String, Int )

When we need to get the String value of an AnswerValue, we do this:

answerValueToString : AnswerValue -> String
answerValueToString answerValue =
    case answerValue of
        AnswerValueNone ->
            ""

        AnswerValueBool bool ->
            if bool == True then
                "true"
            else
                "false"

        AnswerValueString str ->
            str

        AnswerValueCsv str ->
            str

        AnswerValueFloat flt ->
            toString flt

        AnswerValueInt int ->
            toString int

        AnswerValueRangeInt ( str1, int1, str2, int2 ) ->
            str1 ++ "|" ++ toString int1 ++ "|" ++ str2 ++ "|" ++ toString int2

The reason we do it this way is because we have dozens of form fields that we need to store then ultimately pack up into JSON to send downstream. On the DB side, we store each form value as a key/value pair like this:

( "contact.1.phoneNumber.1", "AnswerValueString:917-555-1212" )

So we are storing the type along side the value in the DB.

Hope this helps!

ps regarding the other post about too many messages that are too specific, personally, I’m okay with lots of messages, as it makes the code super easy to troubleshoot and understand. Our type Msg has over 200 variants–and we use a very small number of generic messages to update form data! It just makes sure that you account for all scenarios.

Philosophically, I’d rather write a bit more code today and err on the side of being overly explicit with my intentions than spent hours wracking my brain later when I’ve tried to be a bit too clever and need to figure out what on earth I was thinking 6 months ago. Premature optimization and all that. But just my 2 cents!

2 Likes

Thanks for the perspective very useful.

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