In this series of articles, I am sharing the lessons that I have learned from building reactive applications in Angular using NgRx state management. My introduction explained how I came to use NgRx. Going forward, I will share best practices in the form of an example application, Eternal. Here, we’ll look at the way state management lets you add caching functionality to your code.
If you prefer watching over reading, here is the video version:
Part 1: Cache & Load Status
This pattern ensures that the store does not load data it already has. In other words: It adds a caching functionality.
We create this pattern in two steps. The state gets an additional property called loadStatus, which it uses internally to determine if a request to an endpoint is required.
State management tutorials usually use a load and a loaded action to implement an endpoint request. Our pattern adds a third action called get. Components should only use the get action. The load action is for internal use in state management only.
The diagram below roughly shows in what order actions, effects and reducers work together to load the data against an empty state.
If the state already has data, components can dispatch the get Action as often as they want. It will not result in unnecessary requests:
In our example, there is a component that lists customers and another component that shows a detail form.
Both components need to dispatch the load method. They need the customer data and have to make sure it is loaded.
One could argue that users always follow the path from the overview to the detail view. So it should be enough for only the list view to dispatch the action.
We cannot solely rely on that. Users can have a deep link directly to the form. Maybe some other components in the application directly link there as well.
We do now have the problem that "clicking through the user list" will end up creating lots of unnecessary endpoint calls.
In order to solve that, we introduce a
The data in the store can be in three different states. It can be not loaded, it can load or it is loaded. Additionally, we only want to render our components, when the data is present.
The LoadStatus is a union type with three different values. The state holds it as property and its initial value is set to "NOT_LOADED".
The State changes from
We introduce a further action, that we call
get. The components will only use that action. In contrast to the load method, the
get notifies the store that there is a need for the data.
An effect handles that get method. It checks the current state and, if the state is not "LOADED", it dispatches the actual load action. Note that the load action is now an "internal" action. Components or services should never dispatch it.
Next to the effect that deals with
load action, we also have an additional reducer. It sets the
loadStatus to "LOADING". This has the nice benefit that parallel requests cannot happen. That's secured by design.
The last thing we have to do is to modify our selectors. They should only emit the data if
loadStatus is set to LOADED. Consequently, our components can only render if the data is fully available.
Why can't we just take a null value instead of the
loadStatus as an indicator that the state hasn't been loaded yet? As consumers of the state, we might not know the initial value so we can only guess whether it is null or not. Null may actually be the initial value we received from the backend. Or it may be some other value. Having an explicit
loadStatus value, we can be sure.
The same is also true if we are dealing with an array. Does an empty array mean that the store has just been initialized or does it mean that we really don't have any data? We don't want to show the user "Sorry, no data has been found" when - in reality - the request waits for the response.
Advanced Use Cases only
With complex UIs, the store can easily receive multiple actions in a very short period of time. When different components fire load actions, for example, all of these actions together build up state that some other component wants to display.
A similar use case might be a chain of actions. Again, a dependent component only wants to render when the last action is through.
LoadStatus property, the selector in the component would emit each time the state changes partially. This can result in a user-unfriendly flickering effect.
Instead, the selectors should first check against the
LoadStatus before returning the actual data. That has the nice benefit that the component gets the data only once and at the right time. Very efficient and performant!
If we have multiple components that require the same data and the components are all children of the same route, then we can use a Guard to both dispatch the get action and wait for the data.
In our case, both list and detail are children of "customer". So our "data guard" looks like this:
If you really strive for perfection, you could even extract the dispatching to a component that sits next to the guard. The reason is that guards should be passive and don't have any side effects.
Related Best Practices
In later articles, we will look at best practices related to our caching example. You might also have some context for that data such as an async paginator or a search. Whatever the context, the point is that the frontend possesses a subset of the data which depends on certain "filter parameters" like the current page. If these change, we need to find a way to invalidate the cache. Please read more about it in Context
In another case, we may want to prevent a consumer from manually triggering the load Action with the endpoint call. We can't do it unless we encapsulate the action into an own module and provide an interface for it: Facade.
The upcoming article focuses on architecture. We will find out how to structure our application so that state management can be added as a module and how the components should access that.