Often in web development there is this recurring pattern of having to fetch some data from some server through a rest api, and then show it someway in the UI.
This often includes storing this data somewhere on the client side, either in a store or just a variable you can reference, and this is where the Remote Data type can help.
Usually saving the data would look something like this in JS:
// Javascript
const state = {
data: null,
error: null
}
fetch('/api/data')
.then(res => res.json())
.then(data => state.data = data)
.catch(err => state.error = err)
and showing it on screen:
// React
const MyComp = ({ error, data }) => {
if (error) {
return <div className="error">{error}</div>
} else {
return <div>{data}</div>
}
}
But there are a few problems with this approach:
- What do we show on screen when the data is loading from the server?
- What do we show on screen before we even request the data from the server?
- Do we properly clear the previous error if the data is a success?
- Do we properly clear the previous data if we get an error?
So how can we solve this?
Some might recommend we can add more fields to our state to help represent all the cases, like this:
// Javascript
const state = {
data: null,
error: null,
loading: false,
isAsked: false
}
and in the UI would be similar to this:
// React
const MyComp = ({ error, data, loading, isAsked }) => {
if (!isAsked) {
return <div>Nothing asked yet</div>
}
if (loading) {
return <div>Loading...</div>
}
if (error) {
return <div className="error">{error}</div>
}
if (data) {
return <div>{data}</div>
}
return <div>Some default fallback to be safe</div>
}
But the problem with this is that it becomes too easy for our UI to accidently show the wrong case by forgetting to set loading to false after the data comes back as an error, or forgetting to clear the error if a retry returned a success.
Infact, optimistically the structure above can have a cardinality of 2 x 2 x 2 x 2
which is 16 possible different combinations of states we can be in at any given time. That is a lot of cases to represent.
Let's look at how Elm simplifies this process with needing only 4 cases.
Remote Data in Elm
The Remote Data type can be created manually by writing a custom type like below, or by using a pre-made version from a library like krisajenkins/remotedata:
-- Elm
type RemoteData e a
= NotAsked
| Loading
| Error e
| Success a
and then using this type as your model:
-- Elm
type alias Model = RemoteData String MyData
-- and set the initial model value as NotAsked
init = NotAsked
What we need to understand is that our state can only be one of these 4 types at any time. We can not both be in a Loading state and in a Success state at the same time, or else our UI will be showing 2 different things. This is why our cardinality is only 4 now instead of 16, because there is no way to represent our state more than that.
Using this model we can now create a UI for each case like this:
-- Elm
view model =
case model of
NotAsked -> div [] [ text "Not asked yet" ]
Loading -> div [] [ text "Loading..." ]
Error err -> div [] [ text err ]
Success data -> div [] [ text <| "Here is my data: " ++ data ]
And now whenever we update our state, we never need to worry about clearing previous state or forgetting to flip one field to null - because there is only one field in our model.
This is a great pattern for handling remote data fetching in Elm, but how do we take advantage of this pattern in Javascript? Daggy.
Daggy
There are a few different libraries in Javascript that can help model your application with Algebraic Data Types like we have in Elm with the type
keyword. One popular library is Daggy.
To represent remote data in Daggy, we would create a type like this:
// Javascript
import { taggedSum } from 'daggy'
const RemoteData = taggedSum('RemoteData', {
NotAsked: [],
Loading: [],
Error: ['e'],
Success: ['a']
})
Then once we have our type, the implementation is almost identical to how we would work in Elm.
Our state would only have one field instead of 4, and a cardinality of 4 instead of 16.
// Javascript
const state = {
data: RemoteData.NotAsked
}
// Fetch some data
state.data = RemoteData.Loading
fetch('/api/data')
.then(res => res.json())
.then(data => state.data = RemoteData.Success(data))
.catch(err => state.data = RemoteData.Error(err))
And in our UI, like React, we would have:
// React
const MyComp = ({ data}) => data.cata({
NotAsked: () => <div>Not asked yet</div>,
Loading: () => <div>Loading...</div>,
Error: err => <div>{err}</div>,
Success: d => <div>Here is my data: {d}</div>
})
Using this pattern actually helps move a lot of render logic out of your components in React, like checking fields with if (field) {...}
and instead moves that responsibility to something like a reducer, which will make it a lot easier to run unit tests on.
If you would like to learn more about Algebraic Data Types in Javascript, check out these links:
If you like this article, be sure to follow me!
Top comments (5)
Nice :)
In TypeScript: github.com/devex-web-frontend/remo...
Thank you for the article 💛
Very clear! I like it :)
This is the article that got me into Elm. Thank you for sharing, this is next level. Cardinality is a brilliant way to articulate this approach!
Nice work ;3
Great stuff. Also, the same could be achieved with a discriminated union type, for those working with typescript.