The loading anti pattern is something that I have experience when I need to fetch a data and then display it.
Normally when you want to display a data from an API, there are 5 condition that you want to fulfill.
- Show initial data. It could be a blank screen.
- Show loading indicator.
- Show the result.
- Show different message if the result is empty.
- Show an error if there is one.
So let's try to built this.
First the data structure. Most likely it would look like this
const data = {
items: [],
isLoading: false,
}
The items
is a list I want to display and isLoading
is a boolean so that I know whether it is loading or not.
So lets try to display either the loading or the list component first.
<List>
{isLoading ? <Loading/> : <Items items={items} />}
</List>
So far so good. Now we need to differentiate between the result that have a list and a result that return an empty list. Normally I would do it like this.
<Items>
{items.length > 0 ? items.map(item => <Item>{item.name}</Item>) : <Typography>List is empty</Typography}
<Items/>
Notice that I use items
as an indicator on whether the result from an API is empty or not.
This can be a problem because it will show List is empty
initially even when we haven't fetch the data yet.
One way to solve this is to just set isLoading
to true on the initial data
const data = {
items: [],
isLoading: true,
}
Now let's try to handle a case where an API returns an error. First we need to add extra value to the data.
const data = {
items: [],
isLoading: true,
isFailure: false,
}
Now I can use isFailure
as an indicator to show the error message.
<Box>
{!isFailure ? <List/> : <Error/>}
</Box>
Putting everything together, you have something that looks like this
const data = {
items: [],
isLoading: true,
isFailure: false,
}
const Page = ({data}) =>
(
<Box>
{!data.isFailure ? <List data={data} /> : <Error/>}
</Box>
)
const List = ({data}) =>
(
<Box>
{data.isLoading ? <Loading/> : <Items items={data.items} />}
</Box>
)
const Items = ({items}) =>
(
<Box>
{items.length > 0 ? items.map(item => <Item>{item.name}</Item>) : <Typography>List is empty</Typography}
<Box/>
)
So now that I handle all the condition. You might be wondering what is the problem?
Well, the problem is that I'm trying to describe the state of the data using 3 different value. items
, isLoading
and is isFailure
and this makes your render logic more complex than it should be.
I have nested if
to cater between different state of the data.
!isFailure ? isLoading ? items.length > 0
There can also be an invalid state where both isLoading
and isFailure
can be true
.
The problem lies with trying to use boolean to describe the state of the data. Boolean can only represent 2 state of the data but we know now that the data can have 4 state. Initial, loading, failure and success. This is why you end up with so many value.
So how do we fix this?
I was looking at a video about Elm and one of the talk is about this anti pattern and how to solve them. You can view it here.
Basically, what you should do is to have a single value to represent all the possible state of your data. He suggested the state to be notAsked
, loading
, failure
and success
.
So now you can describe your data like this
const notAsked = 'notAsked'
const loading = 'loading'
const failure = 'failure'
const success = 'success'
const data = {
items: [],
state: notAsked,
}
This pattern solve a couple of problems.
- They can no longer be an invalid state.
- There is a single value to represent the state.
This can make your render logic a lot more simple.
switch (data.state) {
case notAsked:
<Inital/>
break
case loading:
<Loading/>
break
case success:
<List/>
break
case failure:
<Error/>
break
}
If you don't want to watch a video, you can also read his post How Elm slays a UI antipattern.
Although it is geared towards Elm but I believe it can be implemented elsewhere.
Top comments (0)