Most web apps fetch data from a server. Fetching data from a server means that we don't have any data to put in our model when we initialize our app. In this article we will examine different strategies of modelling our app state when working with data from the server.
Dummy Values
We will start with an example to illustrate the problem. Let's say we are building an app that will fetch a list of articles from a server as soon as the app starts up. Our first approach might be to make a model like this:
type alias Model =
{ articles : List Article
}
This looks good, But we have to initialize the model somehow, and since we don't have any articles when starting off (because we haven't made the request yet) we would have to initialize the model like this:
initialModel : Model
initialModel =
{ articles = []
}
This looks fine, but this strategy does pose some problems. One problem is that there is no way to know whether we are still waiting for the server to respond, or whether the server has responded, and there actually are no articles. You could imagine a user being a little scared if we display a message saying "There are no articles" for a second, before we actually get the articles and display them. The user might think that all their articles have been deleted, before realizing that the server was just a little slow to respond.
A Loading Field
One way to distinguish between a request that's still loading, and a request that returned an empty list is to add a loading field to our model, like this:
type alias Model =
{ articles : List Article
, loading : Bool
}
We can then initalize the loading
field to be True
, and when the request is finished, we set the loading
field to be False
. That way we can show nothing, or maybe a loading spinner, until the request is done.
But "loading" and "not loading" are not the only two states our app can be in: what if the request fails? In that case we would have to set the loading
field to be False
, since we are no longer loading, but then the user would definitely think that all their articles were gone. Even if the problem was only that the user lost their internet connection for a second.
An Error Field
To solve this issue, we could add another field to our model, called error
. This field could also be a Bool
, and be set to False
unless the request failed. But maybe we want to keep the actual error in our model, to display different error messages depending on what kind of error we got? In that case we could make the error
field a Maybe Http.Error
, and if the error
field was Nothing
then the request succeeded (or is still loading), and if the error
field is a Just Http.Error
, then the request failed:
type alias Model =
{ articles : List Article
, loading : Bool
, error : Maybe Http.Error
}
This solves our problems, but is kind of unruly to manage. Imagine for instance that we needed to make 3 network requests in our app, then we would have to have 9 fields to manage all this state, which would make the model a little bloated and hard to deal with. Also, there is another problem that this approach causes.
Impossible States
This is an example of a model state that is valid, as far as the compiler is concerned:
{ articles = [ article1, article2 ]
, loading = False
, error = Just Error
}
But what does this mean? We have a list of articles, but we also have an error. Did the request succeed or fail? Your guess is as good as mine. And what about this one:
{ articles = []
, loading = True
, error = Just Error
}
We are still loading, but we have an error? How did we manage to do that? I don't know, but there is probably a bug that makes this happen.
How our app responds to these models is anyone's guess, but it's a recipe for disaster. And while we could just be extra diligent, and double check that we never end up in this state, wouldn't it be better if it was impossible to end up in this state?
Making Impossible States Impossible
In Richard Feldman's brilliant talk from 2016, he explains the concept of making impossible states impossible. Or said in another way: to make sure that only valid states are representable. Our approach thus far does not achieve that.
The problem is that we have three fields that can be changed independently, but that actually are dependent on one another. To solve this, we can replace all three fields with just a single field articles
, of type RemoteArticles
, which we define like this:
type RemoteArticles
= Loading
| Failure Http.Error
| Success (List Article)
This one custom type allows for all the states that we want to be able to represent, and does not allow for any of the states that we want to be unrepresentable. The article request is either loading, or it has failed with an error, or it has succeeded, in which case we have a list of articles.
To use our articles, for instance in the view, we just have to pattern match on the articles
field:
viewRemoteArticles : RemoteArticles -> Html a
viewRemoteArticles remoteArticles =
case remoteArticles of
Loading ->
viewLoadingSpinner
Failure error ->
viewFailure error
Success articles ->
viewArticles articles
This approach serves to both simplify our code, by getting rid of multiple fields to handle one state, and to remove the possibility for a class of bugs in our app.
Conclusion
Modelling remote data with a custom type is a powerful technique, which can be adapted for different scenarios, depending on your app. You could for instance add a variant for NotAsked
, if a request is only triggered by something else. And if you need the result of multiple requests before rendering something, you could combine the states for multiple requests in one custom type.
The Elm package directory has a package for RemoteData
, which contains a custom type and some helper functions, but I usually prefer to make my own when using this pattern in Elm. However you decide to do it, I hope you try out this technique for modelling remote data in Elm.