NgRx: How and where to handle loading and error states of AJAX calls?
Should these states be part of NgRx Store to begin with?
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
TL; DR: It depends… However, we’ll look into the pros and cons of different approaches. If you chose to make the loading/error as part of the state, make sure they are encompassed within a single property.
Not so long ago Michael Hladky started a very interesting discussion on Twitter.
Error is one of the AJAX call states that is frequently placed in the NgRx Store. Others would be Loading and sometimes even Success/Loaded.
interface ResultState {
result: Result,
error: string|null,
isLoading: boolean,
isLoaded: boolean,
}
Why are these states put into Store? Well, this is often driven by UX decisions. For example, the User should be able to see some kind of progress indicator while waiting for calls to complete, or an error message if that call results in one.
Let’s look into each one of them.
Error State
While I was working on this article, Brandon Roberts already wrote a great article about where to handle error state as a response to that tweet:
Handling Error States with NgRx
In that article he went into great depth looking at how error state can be handled. So here I’ll just summaries his arguments and add a few of my own points.
Error-handling in Effects
Does your Component need to know about the error?
That would be my first question. Does a Component have any logic that is dependent on the error result? It might need to display the error message itself or have some conditional logic that hides/disables parts of the DOM if there is one.
When the only component that needs to handle the error is a snackbar or any other type of pop-up, then Effects can handle that error.
Here is how it looks
Error-handling in Components
When Components need info about errors, one of the possible ways to handle them is directly in the Component itself, without making error as part of the Store state at all. Here we are taking advantage of Actions
from the @ngrx/effects package that could be injected into Component, and we could be listening for the Error Action.
The advantages of such an approach are that the error becomes part of the local state of the Component, and as a result, this error is cleaned up when Component is destroyed.
It also helps if we hydrate/rehydrate Store State to and from localStorage via meta-reducer, we wouldn’t be saving/restoring the State with Error. That actually makes sense — we don’t want to show an error to a User, who just opened up the page. Of course, there are some steps that could be taken to prevent that (e.g. filter what parts of State we sync).
However, the number of disadvantages is a lot larger. If there are 2+ Components that rely on that Load Movies
response, they all would have to re-implement this error handling logic. What’s making it even more complicated is: when one of those components dispatches another Action to refresh the data, it will only clear its own error. The rest of the Components will continue to display it.
Brandon also adds other disadvantages, such as re-introduction of side effect code into your component and a more complicated test setup.
So whether to store the error locally or in the NgRx Store depends on your use case, and the latter might be a safer choice.
Loading State
Spinners or other loading indicators are frequently wired up to isLoading
or pending
states in NgRx. Some claim that it shouldn’t be stored as a separate property and it is a derived state, which is determined by the presence of the data itself — if it’s not there it might be loading.
const isLoading = createSelector(
getProductsState,
state => !!state.products,
);
However, this is where UX requirements come into play.
Loading while displaying cached results
For example, we have a cached list of products that we want to show while sending an AJAX request to check for any updates. This is the case where just the presence of data in the store is not enough and an explicit isLoading
state is necessary.
Loading data in chunks
Another example is when a response comes in “paginated” chunks. When the first chunk arrives the data is immediately displayed. But, we would still want to keep showing some kind of a loading indicator until all of the chunks are loaded.
Success/Loaded State
interface ResultState {
result: Result,
error: string|null,
isLoading: boolean,
isLoaded: boolean,
}
Success state is a tricky one. Personally, I never had to store it; however, I can think of one scenario where it might be useful.
In many cases, the data that I’m working with is a collection of some sort. My result is frequently an array — Result[]
. That’s why even when the result comes empty, I can easily distinguish between the initial/loading state (result would be null
) and the loaded/success state (result would be []
— empty array).
In the case where the result is a primitive it is still possible to tell the initial/loaded states apart: it’s fine to set the initial state to null
for booleans, numbers or strings.
It gets trickier when a result is an object — this is where success state might be used.
The alternative would be to type the result as result: Result|null|{}
where an empty object might mean the response came empty. But, there has to be a consensus among team members on what means what.
All-in-one State
With so many additional properties that have to accompany a single AJAX call, could there be a better way?
This is the discussion that we had in our team and the conclusion that we arrived at is that we could combine them all under a single state property, and still get the error message if needed. It also helps eliminate the invalid states, such as { isLoading: true, error: 'Failed', isLoaded: true }
With this all-in-one interface our ResultState turns into the following
interface ResultState {
result: Result,
callState: CallState,
}
Our reducer becomes cleaner and less prone to errors.
Helper getError
function can be used in the selectors.
As you can see, the selectors stay almost the same while the reducer is dramatically simplified.
Conclusion
While in many cases it is possible to avoid putting error/loading states into the Store, should you have more intricate requirements from UX, consider storing them as a single property.
Are you interested in NgRx? Do you want to learn everything from the basics to advanced techniques?
If you are in San Francisco / Bay Area, I’ll be doing the popular 2-day NgRx workshop on October 23–24, 2019. Tickets and more info is available here: https://www.eventbrite.ca/e/ngrx-in-san-francisco-from-start-to-advanced-concepts-in-2-days-tickets-74313759455?aff=aid
Let me help you take your state management skills to the new heights! Hope to see you there!