I often see questions on how to scale Elm applications. Similarly, I see questions relating to creating reusable views.

This post will walk through a simple example, and while this example won’t exhaustively answer all of your questions, it will hopefully put the reader in the right mindset for scaling Elm.

Consider a hypothetical application that allows you to adjust the account balance of a couple of accounts.

type alias Account =

{ ownerName : String

, balance : Int

} type alias Model =

{ account1 : Account

, account2 : Account

}

Let’s take a look at a minimally viable view required to support this functionality.

view : Model -> Html Msg

view model =

div []

[ text <| "Account for " ++ model.account1.ownerName

, div []

[ div []

[ button

[ onClick DecrementBalance1 ]

[ text "-" ]

, div

[ countStyle ]

[ text (toString model.account1.balance) ]

, button

[ onClick IncrementBalance1 ]

[ text "+" ]

]

]

, text <| "Account for " ++ model.account2.ownerName

, div []

[ div []

[ button

[ onClick DecrementBalance2 ]

[ text "-" ]

, div

[ countStyle ]

[ text (toString model.account2.balance) ]

, button

[ onClick IncrementBalance2 ]

[ text "+" ]

]

]

]

The above code would produce a view that’d look something like this.

When one of those buttons is clicked our update function handles the Msg how you might expect.

...

DecrementBalance1 ->

let

account =

model.account1 updatedAccount =

{ account | balance = account.balance - 1 }

in

{ model | account1 = updatedAccount } ! [ Cmd.none ]

...

While this approach works, it is not scalable. If I want to manage more accounts the Model grows undesirably as does the inferred Msg type. Also, what if I wanted to reuse that view somewhere else with non-account data? Or what if I wanted to swap that view out with a different view? What then?

If you’d like to see the full code so far here is a link.

Time To Scale!

We decide that we need to make some changes in order to make the application more scalable. Not to mention, there are a couple of opportunities for refactoring anyway.

Let’s start with the Model. This is the easiest part to scale. We’re going to turn two hard-coded accounts into a List of accounts.

type alias Account =

{ ownerName : String

, balance : Int

} type alias Model =

List Account

Note: In a production application you would probably have unique IDs to work with in which case you have an alternative option to store these Accounts in a Dict keyed by ID.

Excellent. That was easy. Now let’s look at our Msg.

type Msg

= DecrementBalance1

| IncrementBalance1

| DecrementBalance2

| IncrementBalance2

Already we can tell that there’s too much duplication even before considering what’d it take to scale it. Let’s fix that.

type Msg

= DecrementBalance Int

| IncrementBalance Int

With the Int representing the index of the Account in a list we managed to accomplish both refactoring our Msg and making it scalable at the same time!

Now on to the update function. The changes necessary here are fairly mechanical.

DecrementBalance accountIndex ->

(decrementBalance model accountIndex, Cmd.none) .. decrementBalance : Model -> Int -> Model

decrementBalance model accountIndex =

List.indexedMap

(\index account ->

if index == accountIndex then

{ account | balance = account.balance - 1 }

else

account

)

model

All we’re doing here is finding the account to update by index and then updating that account.

Finally, we get to the view function. The first incremental refactor we can do is to extract out the duplicated code into another function and map over the list of accounts with this new function.

view : Model -> Html Msg

view model =

div [] <| List.indexedMap counterView model counterView : Int -> Account -> Html Msg

counterView accountIndex account =

div

[]

[ text <| "Account for " ++ account.ownerName

, div []

[ button

[ onClick <| DecrementBalance accountIndex ]

[ text "-" ]

, div

[ countStyle ]

[ text (toString account.balance) ]

, button

[ onClick <| IncrementBalance accountIndex ]

[ text "+" ]

]

]

Nice! We’ve already made a drastic improvement by making the top level view more readable and by removing duplicate code.

But we’re not done yet. This view is only reusable in the context of displaying an account and its balance. Let’s assume we‘re confident that we’re soon going to be reusing this view again but for entirely different purposes. How does this view scale? How do we make it reusable?

You would do the same thing you would do to make any function more reusable, because that’s all it is anyway, a function. We’re going to extract out the specifics and create a more general interface to the function.

counterView : Msg -> Msg -> String -> String -> Html Msg

counterView decMsg incMsg heading value =

div

[]

[ text heading

, div []

[ button

[ onClick decMsg ]

[ text "-" ]

, div

[ countStyle ]

[ text value ]

, button

[ onClick incMsg ]

[ text "+" ]

]

]

We have now completely decoupled this view from the Account type. It‘s very versatile and reusable and you can reuse it as long as you provide the following:

A Msg to fire when the “-” button is clicked

to fire when the “-” button is clicked A Msg to fire when the “+” button is clicked

to fire when the “+” button is clicked A String heading to display above the buttons

heading to display above the buttons A String value to display between the buttons

Is that it?

Nope. It’s not all puppy dogs and rainbows yet. Our view function got ugly.

view : Model -> Html Msg

view model =

div [] <|

List.indexedMap

(\accountIndex account ->

let

decMsg =

DecrementBalance accountIndex incMsg =

IncrementBalance accountIndex heading =

"Account for " ++ account.ownerName value =

toString account.balance

in

counterView decMsg incMsg heading value

)

model

Even though the function is doing something very simple it’s taking 20+ lines to do it and we’re mixing data transformation with Html invocations which is mentally taxing to try and keep straight, even in this contrived example.

There are different ways to solve this. I obviously have a favorite, but this example is small enough that a simple helper function will suffice.

modelToViewModel : Model -> List CounterViewModel

modelToViewModel model =

List.indexedMap

(\accountIndex account ->

let

decMsg =

DecrementBalance accountIndex incMsg =

IncrementBalance accountIndex heading =

"Account for " ++ account.ownerName value =

toString account.balance

in

CounterViewModel decMsg incMsg heading value

)

model view : Model -> Html Msg

view model =

let

viewModels = modelToViewModel model

in

div [] <| List.map counterView viewModels -- Code below this line would go in a separate module

-- module CounterView exposing (counterView, CounterViewModel) type alias CounterViewModel =

{ decMsg : Msg

, incMsg : Msg

, heading : String

, value : String

} counterView : CounterViewModel -> Html Msg

counterView { decMsg, incMsg, heading, value } =

div

[]

[ text heading

, div []

[ button

[ onClick decMsg ]

[ text "-" ]

, div

[ countStyle ]

[ text value ]

, button

[ onClick incMsg ]

[ text "+" ]

]

]

Much better. The counterView is still completely reusable. The data it requires is made explicit through specifying a CounterViewModel. Our view function has simplified. Lastly, we’ve separated out the data transformation part into a simple helper function which can be tested in complete isolation without having to deal with Html getting in the way.

One could easily add a new Msg constructor and button to add new accounts, and this example will still work just fine.

Here is a link to the refactored example, and once more here is a link to the original.

Takeaways

There are a few points worth noting when comparing how this example scaled compared to how it could have been scaled.

The reusable counterView could show other data that is completely unrelated to the Account type if we desired.

could show other data that is completely unrelated to the type if we desired. The Msg , the Model , and the update function are strictly focused on managing the single source of truth and are completely agnostic to the underlying reusable counterView function. Said differently, counterView is not leaking its opinions into other parts of the application.

, the , and the function are strictly focused on managing the single source of truth and are completely agnostic to the underlying reusable function. Said differently, is not leaking its opinions into other parts of the application. Similar to the above point, if at any point in the future we wanted to swap out counterView with something entirely different, we could and with minimal impact to the rest of the application.

Please reach out if you have questions or if you other ideas on how to scale this example!