We fight with too large Msg in Elm applications
- Tutorial
According to Elm Architecture, all application logic is concentrated in one place. This is a fairly simple and convenient approach, but with the growth of the application, you can see a function update
with a length of 700 lines, Msg
with a hundred constructors and Model
that does not fit on the screen.
Such code is pretty hard to learn and often maintain. I would like to demonstrate a very simple technique that will improve the level of abstractions in your application.
Let's look at a simple example.
First, create a small application with just one text box. Full code can be found here .
type alias Model =
{ name : String
}
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Name", value model.name, onInput ChangeName ] []
]
type Msg
= ChangeName String
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeName newName ->
{ model | name = newName }
The application is growing, we are adding a surname, "about ourselves" and the "Save" button. Commit here .
type alias Model =
{ name : String
, surname : String
, bio : String
}
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Name", value model.name, onInput ChangeName ] []
, br [] []
, input [ placeholder "Surname", value model.surname, onInput ChangeSurname ] []
, br [] []
, textarea [ placeholder "Bio", onInput ChangeBio, value model.bio ] []
, br [] []
, button [ onClick Save ] [ text "Save" ]
]
type Msg
= ChangeName String
| ChangeSurname String
| ChangeBio String
| Save
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeName newName ->
{ model | name = newName }
ChangeSurname newSurname ->
{ model | surname = newSurname }
ChangeBio newBio ->
{ model | bio = newBio }
Save ->
...
Nothing remarkable, all is well.
But the complexity increases dramatically when we decide to add another component to our page that is completely unrelated to the existing one - the form for the dog. Commit .
type Msg
= ChangeName String
| ChangeSurname String
| ChangeBio String
| Save
| ChangeDogName String
| ChangeBreed String
| ChangeDogBio String
| SaveDog
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeName newName ->
{ model | name = newName }
ChangeSurname newSurname ->
{ model | surname = newSurname }
ChangeBio newBio ->
{ model | bio = newBio }
Save ->
...
ChangeDogName newName ->
{ model | dogName = newName }
ChangeBreed newBreed ->
{ model | breed = newBreed }
ChangeDogBio newBio ->
{ model | dogBio = newBio }
SaveDog ->
...
Already at this stage, you can notice that it Msg
contains two "groups" of messages. My "programmatic flair" suggests that such things need to be abstracted. What will happen when 5 more components appear? What about subcomponents? It will be almost impossible to navigate in this code.
Can we introduce this additional layer of abstraction? Of course !
type Msg
= HoomanEvent HoomanMsg
| DoggoEvent DoggoMsg
type HoomanMsg
= ChangeHoomanName String
| ChangeHoomanSurname String
| ChangeHoomanBio String
| SaveHooman
type DoggoMsg
= ChangeDogName String
| ChangeDogBreed String
| ChangeDogBio String
| SaveDog
update : Msg -> Model -> Model
update msg model =
case msg of
HoomanEvent hoomanMsg ->
updateHooman hoomanMsg model
DoggoEvent doggoMsg ->
updateDoggo doggoMsg model
updateHooman : HoomanMsg -> Model -> Model
updateHooman msg model =
case msg of
ChangeHoomanName newName ->
{ model | name = newName }
-- Code skipped --
updateDoggo : DoggoMsg -> Model -> Model
-- Code skipped --
view : Model -> Html Msg
view model =
div []
[ h3 [] [ text "Hooman" ]
, input [ placeholder "Name", value model.name, onInput (HoomanEvent << ChangeHoomanName) ] []
, -- Code skipped --
, button [ onClick (HoomanEvent SaveHooman) ] [ text "Save" ]
, h3 [] [ text "Doggo" ]
, input [ placeholder "Name", value model.dogName, onInput (DoggoEvent << ChangeDogName) ] []
, -- Code skipped --
]
Utilizing the Elm type system, we divided our messages into two types: human and canine. Now the threshold for entering this code will be much easier. As soon as some developer needs to change something in one of the components, he will be able to immediately determine by the type structure which parts of the code he needs. Need to add logic to dog information retention? Look at the messages and start a search on them.
Imagine your code is a huge reference. How will you search for the information you are interested in? By table of contents (Msg and Model). Will it be easy for you to navigate the table of contents without dividing into sections and subsections? Unlikely.
Conclusion
This is an extremely simple technique that can be used anywhere and is pretty easy to embed in existing code. Refactoring an existing application will be completely painless, thanks to static typing and our favorite elm compiler.
Having spent only an hour of your time (we spent less than 20 minutes on each application on our project), you can significantly improve the readability of your code and set the standard for how to write it in the future. Not the code that is easy to correct errors is good, but the one that prohibits errors and sets an example of how the code should be written.
Exactly the same technique can be applied to Model
, highlighting the necessary information in types. For example, in our example, the model can be divided into only two types: Hooman
and Doggo
, reducing the number of fields in the model to two.
God save the Elm type system.
PS repository with code can be found here if you want to see diffs