I’d like to share some of the techniques we’ve been using in Gizra to structure and wire our Elm apps. Notably, highlighting the elm-fetch package conceived by @rgrempel. This package allows you think about the relation between your model to HTTP fetching in the same manner you think about the relation between your model and the view function.

Background

In Gizra, after more than four years of building Elm apps for different clients, we’ve gathered quite a few know hows. From (small) web apps on (big) websites, like the United Nations - where each UN country member has their own site on one huge platform; to completely offline webapps for medical records in rural Rwanda; and to (extremely) online webapps, that help sell auction items at hard to grasp prices.

An official UN site with Elm as a tiny embedded app showing images from Flickr
An online auction selling a single item for over 1.2 million Euros

Demo app & Concepts

Let’s start with what our demo app does. It loads the top stories from HackerNews API, and once it has a list with the twenty top item IDs, it starts fetching the items in batches of three. That is, it sends three separate requests, with each item ID. Once those three items are loaded, it goes on to the next batch.

Demo of the app with network throttled to make it load slower

How would you normally implement this? There might be some different ways, but one of the beauties of Elm, is that in a way there’s usually just one way to do things.

Before the introduction of the fetch package, we would have done something like this: Upon init of our app, we’d call some FetchTopStories msg, and on HandleFetchTopStories if we got a result, we’d have some logic to batch the Items together and call FetchItem ItemId msg.

And the above is indeed along the lines of what we have implemented

In a way there isn’t anything terribly wrong with that, but conceptually, Elm allows us to do better. Actually, the way we think about a view function in Elm helps us move in that direction.

Let’s think about a view function in Elm. While it’s what end users see, it’s almost the most boring part of the app. We first model the problem, thinking about the Model and Msgs that would change the state, and the view is just some HTML that is rendered upon the model it’s being fed.

We can say it differently: whatever state we’re going to feed the view we’re always going to get the same result. I could give you a pen and paper and ask you to write down the HTML we’d get given a certain state, and you’d have all the things you’d need to answer that.

Now let’s think about the case of fetching data from a remote server. If I’d give you a pen and paper, along with the current model - would you be able to tell me which HTTP requests are going to be issued? Probably not. Because that info is being sent via Cmds, or triggered by for example some onClick event.

Wouldn’t it be great if we’d be able to reason with our data-fetching by looking at model, just the same as we’re doing when we think about the view function? This is where elm-fetch comes in. It slightly changes the way we’re “modeling the problem” thanks to a conceptual shift - we’re now thinking about how to model the idea of “when do we need to fetch this thing”.

ModelBackend & Pages

Ok, we’re almost ready to show how the fetch is implemented. Lets cover another aspect we came to enforce in our apps - data coming from the backend, should live completely separate from UI related state. The separation is probably clear. Data from the backend, is just data. It has no knowledge about it being displayed or not - the same as your data that lives in the backend’s DB. That’s why we have our own ModelBackend.

You can think of it as a client cache layer - where we hold the data we got from the server (and as such, we may invalidate the cache). Then we have the concept of a Page. That page can have it’s own model, holding data related to the UI - for example, which Item is now selected by the user. Item selection has no meaning on the backend. It’s just a UI state thing.

So we covered the separation of concerns. Now we can think about who is responsible for deciding which data to fetch. Is it the backend? Well, we will get the data from the backend, but it has no knowledge of what and when to fetch. But every Page does know. That is, every page a user is navigating into is displaying some data. So it’s actually that page that knows which data the user is expecting to see, and can request it.

Let’s just clarify the “Page should request it” part. The page knows it wants to display some data, thus knows the backend should fetch it, but the page doesn’t know how to actually fetch it. That is, any HTTP request should come from the Backend and not from the Page.

Fetch in Action

We’re now ready to see how it’s done. Well, luckily, it doesn’t take much to wire this new fetch concept to your existing app.

  1. Let main know about your fetch as seen in this diff of Main.elm

main =
  Browser.element
      { init = App.Update.init
-     , update = App.Update.update
+     , update = Update.Fetch.andThenFetch App.Fetch.fetch App.Update.update
      , view = App.View.view
      , subscriptions = App.Update.subscriptions

  1. Then you add an App level fetch that in turn will call the active page’s fetch logic.

  2. Finally have a look at the Page level fetch. Here you’d notice we check the state of the BackendModel, and rely on RemoteData to make sure we’re not issuing the same Msg over and over again.

Now what if we’ve wanted to clear all items, or just a specific item. Obviously there would be some onClick in the Page’s view. However, as we say the Page should just be aware of the ModelBackend - or said differently, it gets it as a read only argument, then you’d understand why all that ClearItems on the Page does is call the ClearItems Msg of the backend.

Summary

That’s all there is to it really. It’s a small conceptual change, but with several advantages. It allows us to think about remote data fetching based on a specific state, without caring about how we transitioned into that state. That’s especially important when the app becomes bigger, and suddenly different Msgs may end up invalidating the ModelBackend data.

Your fetch function just needs to look at the data, and decide:

  1. Is theitems property RemoteData.NotAsked? Then let’s call FetchTopStories.
  2. Is it already RemoteData.Loading? Then no need to try and fetch - it’s already happening.
  3. Is there is another property called modelBackebd.images but our page doesn’t care about it? Then just ignore it.
amitaibu's profile

Amitai Burstein

@amitaibu