Upgrade your Elm Views with Selectors

A strategy inspired by reactjs/reselect

Views that do too much

If your Elm applications are like mine then many of your view functions might have the following type annotation.

view : Model -> Html Msg

On initial inspection the type annotation is concise and sensible. Give the view a Model and it’ll crank out some Html capable of producing a Msg.

But when we dive deeper into its implementation we’ll start to notice a pattern.

view : Model -> Html Msg
view model =
let
{-
            20+ lines of data massaging

-}
in
div
[ class "my class helpers" ]
[ child1 child1Model
, child2 child2Model False ]

Ignoring the poor naming choice and the super contrived example, that might look familiar.

It’s a pattern I saw repeated over and over again in our code and at some point the appropriate perspective occurred to me. This view is doing too much. This view is taking in a Model, transforming it into one or more view models, and producing Html. Ideally, this view takes in a view model and produces Html, and it does nothing else.

Inspiration from JavascriptLand

As it turns out, there’s a useful library called reselect that solves this problem in apps using Redux. Reselect introduces the idea of a selector.

A selector is a memoized function that transforms several inputs into a single output.

At work, we use reselect to combine data from several Redux stores to produce view models that only get recomputed whenever relevant parts of the store change. These selectors have helped us tremendously in keeping our Redux stores simple and our view components devoid of data transformation.

Can we use selectors in Elm?

Well, I wouldn’t be writing this blog post if we couldn’t. So let’s cut to the chase and get to the good bits.

We have this:

view : Model -> Html Msg

But we want this

view : ViewModel -> Html Msg

So we want a view to accept a ViewModel, which means we need to tell Elm how to transform a Model into a ViewModel to keep things happy with The Elm Architecture. Let’s write this out more generically.

a == Model
b == ViewModel
c == Html Msg
We want our view to be (b -> c)
But Elm is expecting (a -> c)
So we need to provide (a -> b)

We ultimately need something that allows us to do this:

?? : (b -> c) -> (a -> b) -> (a -> c)

If you don’t recognize that signature then we can open up the fancy search and plop that in.

The first result is <<, the function composition operator that is part of elm-lang/core. Excellent.

Views 2.0

The function composition operator allows us to bring the selector concept in to help clean up our views. First, a look at the selector.

-- TodoSelector.elm
type alias ViewModel =
{ filteredItems :
List
{ name : String
, isChecked : Bool }
, errorMessage : String
, submitButtonData :
{ text : String
, clickMsg : Msg }
}

todoItemsSelector : Model -> ViewModel
todoItemsSelector model =
let
filteredItems =
model.todoItems
|> List.filter (\item -> item.created > model.selectedTime)
|> List.map (\item -> {name = item.name, isChecked = False }
        errorMessage =
Helper.formatError model.error
        (submitButtonText, clickMsg) =
if model.page == Edit then
("Update", UpdateTodos)
else
("Delete", DeleteTodos)
in
{ filteredItems = filteredItems
, errorMessage = errorMessage
, submitButtonData =
{ text = submitButtonText
, clickMsg = clickMsg }

And a look at the view..

-- TodoView.elm
view : Model -> Html Msg
view =
todoItemsView << todoItemsSelector

todoItemsView : ViewModel -> Html Msg
todoItemsView viewModel =
div []
[ text viewModel.errorMessage
, ul [] (List.map todoItemView viewModel.filteredItems)
, button [ onClick Cancel ] [ text "Cancel" ]
, button [ onClick viewModel.submitButtonData.clickMsg ] [ text viewModel.submitButtonData.text ] ]

todoItemView : { name : String, isChecked : Bool } -> Html Msg
todoItemView {name, isChecked} =
li
[ classList [ ( "highlight", isChecked ) ] ]
[ text name ]

There are a few benefits that come from this.

  1. Views are easier to read because they are doing less. Instead of a view doing a lot of data transformation in a big let block it is simply producing Html.
  2. View models stay out of Model. One may be tempted to put extra view model data in Model but that is technically duplicated state which should generally be avoided.
  3. The data transformation code is more testable. This is true simply because Html Msg is out of the equation. The selector is transforming data from one shape to another which is much easier to test in isolation than if that logic was part of a view.

I’m happy to hear how others are solving this problem or if you see issues with this approach. It’s working well so far but we haven’t used this approach extensively yet.

Part 2