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.
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.
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 Msg
s 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 Cmd
s, 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.
- Let
main
know about yourfetch
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
Then you add an App level fetch that in turn will call the active page’s fetch logic.
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 sameMsg
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 Msg
s may end up invalidating the ModelBackend
data.
Your fetch
function just needs to look at the data, and decide:
- Is the
items
propertyRemoteData.NotAsked
? Then let’s callFetchTopStories
. - Is it already
RemoteData.Loading
? Then no need to try and fetch - it’s already happening. - Is there is another property called
modelBackebd.images
but our page doesn’t care about it? Then just ignore it.