Elm Tricks from Production–Declarative, Bug-Free User Interfaces with Custom Types

Posted on July 20, 2020 by Riccardo. Originally posted at https://blog.lunarlogic.io/2019/elm-tricks-from-production-custom-types/.

Elm Tricks from Production (Series)

Elm Tricks from Production–Intro Elm Tricks from Production–Migrating from Angular v1 to Elm Elm Tricks from Production–Declarative, Bug-Free User Interfaces with Custom Types Elm Tricks from Production–Adding Event Listeners to DOM Nodes that do not yet Exist Elm Tricks from Production–Automated Testing is Just Another Tool Elm Tricks from Production–From Angular v1 to Elm in 4 Days

There are 4 possible statuses when it comes to fetching data in a frontend application:

  • before fetching
  • while fetching
  • successful fetch
  • failed fetch

In JavaScript it’s easy to either forget handling some statuses or to declare cleanly what to render on screen for each status. This opens the gates to a lot of bugs. Ever been on a page with a spinner that never disappears?

Elm solves both the problems defined above in perfect functional-programming style, adding a type. In particular, let’s say we want to create a new cool webapp. It will show a random number every time the page is refreshed, cool right?! We can use a public API to fetch the random number. Also, we need to choose a type, let’s go with Int. Unfortunately, the random number API is really slow so we need to show a message while fetching. We could use 0 to mark the fact that we still don’t have the random number:

case number of
  0 -> Html.text "Loading..."
  i -> Html.text ("The random number is: " ++ String.fromInt i)

But this is not a good solution: if the API returns 0 as a random number we will be showing the loading message forever! Let’s try with Maybe Int:

case maybeNumber of
  Nothing -> Html.text "Loading..."
  Just i  -> Html.text ("The random number is: " ++ String.fromInt i)

Cool, now we can distinguish between not having the number or having it. What if we got an error though? Let’s add a flag for that:

case (maybeNumber, hasFailed) of
  (Nothing, False) -> Html.text "Loading..."
  (Just i, False)  -> Html.text ("The random number is: " ++ String.fromInt i) 
  (Nothing, True)  -> Html.text "Could not fetch random number" 
  (Just i, True)   -> WTH??

Problem is there’s a combination that does not make sense at all (i.e. (Just i, True)): we fetched a number but there’s an error. Custom type to the rescue:

type RemoteData
  = Loading
  | Success Int
  | Failure

case remoteDataNumber of
  Loading   -> Html.text "Loading..."
  Success i -> Html.text ("The random number is: " ++ String.fromInt i) 
  Failure   -> Html.text "Could not fetch random number"

Now, let’s say that instead of loading the random number every time the page is refreshed, we want to start fetching as soon as a button is clicked:

type RemoteData
  = NotAsked
  | Loading
  | Success Int
  | Failure

case remoteDataNumber of
  NotAsked  -> Html.text "Click the button!"
  Loading   -> Html.text "Loading..."
  Success i -> Html.text ("The random number is: " ++ String.fromInt i) 
  Failure -> Html.text "Could not fetch random number"

The beauty of this solution is that, as soon as we declare our random number to be of RemoteData type, the Elm compiler will enforce checking all the branches. Also, it’s really easy to see what the app is doing depending on the status of the request.

AirCasting uses the pattern shown above extensively. In particular, to avoid reinventing the wheel we used RemoteData. One example is the function that takes care of rendering either the sessions list or the selected session at the bottom of the map page:

viewSessionsOrSelectedSession selectedSession =
        div []
            [ case selectedSession of
                NotAsked ->
                    viewSessions

                Success session ->
                    viewSelectedSession (Just session)

                Loading ->
                    viewSelectedSession Nothing

                Failure _ ->
                    div [] [ text "error!" ]
            ]

In other words, if the request for the selected session is not ongoing, show all the sessions. If the selected session is loading show its view while waiting for the data to come. If the request was successful show the selected session view for it. If the request failed show the error message.

If you are not using Elm you can still apply the pattern successfully. Unfortunately, there won’t be a nice compiler helping guaranteeing an error free application 😉.


Support my work by tweeting this article! 🙏