Dealing with Remote Data in JS
Recently I’ve been playing with Elm a lot, and in the process stumbled upon this great article —
In it, the author addresses the problem of loading states and remote data through the use of Elm union types. While Elm is great to work in, not everyone has the luxury of using a strongly typed language in the browser. Fortunately, there’s a way to do this in Javascript with much of the same benefits as in Elm…
Acquiring Union Types
A union type is a type that can be represented by one or more concrete values, which may or may not contain more values. In our case, our remote data can be in one of four states, represented as such:
type RemoteData e a
= NotAsked
| Loading
| Failed e
| Success a
It wouldn’t take long to put together a RemoteData module that would let us to represent our union type, but for the sake of keeping this article short I’ll use this union-type module.
We can now define our type —
import Type from 'union-type'const RemoteData = Type({
NotAsked: [],
Loading: [],
Failed: [e => e instanceof Error],
Success: [a => true]
})export default RemoteData
The type constructors Failed
and Success
are defined with functions for validating their arguments. In this case, Failed
accepts an Error
and Success
will accept an argument of any type.
Using Our Union Type
Now comes the fun part! I’ll be using React + setState to demonstrate, but this concept would of course work with any view library and state management setup. In this example, we’ll build a mock search engine home page.
Our app will consist of a simple component, SearchPage
, which our users will use to search for things and view the results. In addition, we want to have a loading view while a search is in progress, and a failure view so that users will be informed when things go wrong.
To start, let’s define our component and initial state —
class SearchPage extends Component {
constructor (props) {
super(props)
this.state = { search: RemoteData.NotAsked }
}
}
Perfect; now, let’s add a case
expression in the our render method to “pattern match” on the search value (exactly like we would in Elm). When we’re in the NotAsked state, we’ll display our Search.Home
view —
render () {
return this.state.search.case({
NotAsked: () => (
<Search.Home />
)
})
}
Once a user enters a query, we want to display an “in progress” view and start fetching search data. Again, we pattern match on our search state to know when to render the view. Here is what that may look like inside of our component —
searchFor (query) {
this.setState({ search: RemoteData.Loading })
server.doSearch(query)
}render () {
return this.state.search.case({
NotAsked: () => (
<Search.Home onSearch={(query) => this.searchFor(query)} />
),
Loading: () => (
<Search.InProgress />
)
})
}
We still need to handle the result of our search. Depending on whether or not our search Promise resolves, we’ll wrap it in a Success
orFailed
constructor, then set it on our state. Finally, we just add branches to our case expression to account for these states —
searchFor (query) {
this.setState({ search: RemoteData.Loading })
server.doSearch(query)
.then(RemoteData.Success, RemoteData.Failed)
.then(result => this.setState({ search: result }))
}render () {
return this.state.search.case({
// ...
Failed: (error) => (
<Search.Failed reason={error.message} />
),
Success: (results) => (
<Search.Results data={results} />
)
})
}
All in all, here is what our finished component looks like:
Summary
And there you have it! All in all, I find this to be a nice way of dealing with remote data, for a number of reasons:
- Clearly separating concerns between our four possible states makes our code incredibly legible, and each sub-component can be maintained in isolation.
- There are no sloppy ternary expressions or null-checks littering our JSX, trying to figure out if we should render a search bar, a spinner, some error text, or our search results.
- It’s always clear what state our application is in at any time, and what data we have access to.
- Having to account for all cases of a given type may lead to better UX, since it’s more difficult to “forget” to add a loading or error state.
I’ve only recently started playing with this idea in my Javascript code, so I’m curious what other people think. Is this a technique you would consider using? Do you see any potential downsides? Let me know in the comments!
For anyone who found this interesting, and hasn’t yet been introduced to functional programming or strongly typed languages, I highly recommend looking into Elm (elm-lang.org).