![State Aware Resources. A reactive solution.](https://www.sngular.com/images/0/468/hero_s/state-aware-resources-sngular-blog.png)
State Aware Resources. A reactive solution.
8 de septiembre de 2024
Oftentimes, the applications we develop have one or more asynchronous events occurring while users interact. This might include network requests, a long-lasting background task, or a persistence operation. This is not unusual, and fetching is always handled magically by our favourite library or framework of choice.
The more difficult problem is how to provide user feedback during the waiting times. Each application design will have its own way of entertaining its users while a process is pending, but it often falls into a pattern based on states.
It is up to the developer to identify these states and control them in the application.
We can take advantage of the patterns used on the user experience layer to abstract the logic related to these events and reduce boilerplate. In any modern framework, the view can react to changes on a shared state (e.g. service, presenter, store).
Therefore, using an immutable object offering a declarative interface would allow for a seamless connection between the view and the state.
In this post, I will explain one way to approach this. I find the solution to be elegant, simple, and clean. We will see how it applies to an Angular project, taking advantage of the RxJs library as well.
Since the design is agnostic to the framework, this solution could be easily applied to other scenarios. Regardless, I encourage you to check out the following section and arrive at your own conclusions.
Problem-solving
Thinking it through
In this section, I will break down this solution to uncover an implementation strategy. If you're only interested in the code, you can skip to the next section or go to the conclusions, where I share my solution for this on Github.
First, let’s take a look at what our usual workflow looks like. Assume we are making an HTTP request and we are expecting the details for a “To Do” item.
When we start the request, we are asynchronously waiting for the response to come back, and the user needs to see this is happening. In order to do this, we might use a boolean indicating whether we are loading the resource or not. We then have our first state, which is the loading state.
In the best-case scenario, the server returns the expected resource successfully. We can store that resource and that will be enough to know we are in a new state. Let’s call this our success state.
But what happens if the request fails? The user might need to be authenticated, the server could be down or invalid user-input might cause a bad request. By storing the error returned by the server, we will know we have reached the failure state.
And then we have the empty state. This is the case where we receive a success response, but the data is not there. This could be, for example, because of an empty list. Normally, instead of showing an empty page, we would add some feedback for the user to see. We tend to treat this as an empty state.
We now have four defined states, with one initial state resulting in three possible outcomes. Every time we make a request, we would have to check for the changes in these states and give user feedback accordingly.
But we also have to make sure that, regardless of the outcome, we can clean up for any state change that the transition from "loading" to "not loading" may require. Hence, we can add an extra "always" event along with our state changes.
Now that we have identified the behaviour, let’s apply a solution in which we reduce the amount of boilerplate necessary to achieve this.
Let’s start coding
If we expect our view to be reactive, we can implement a wrapper that holds our possible resource or error and that can also control the loading state. The view would later receive this wrapper as an immutable object that changes due to our state management and would adapt accordingly. This way our view would not have to do any state checks or state management at all.
The constructor is the only way to define the data in the resource wrapper. An immutable wrapper forces new objects to be generated on state change, making sure that this would fit in a reactive solution.
Now that we are wrapping the possible outcomes, let’s make sure that the wrapper itself is capable of indicating the state that our action is in.
The boolean flag for the loading state is enough, but the rest of states need extra checks. It is important that only one of each can return true at a given time. This way we can obtain a specific state without having to add extra internal states on the wrapper.
The getters are private to expose a different interface and guide the developer to the possible outcomes. Likewise, they reduce boilerplate checks throughout the application.
The function accepts multiple callbacks. We have already ensured that only one of them will trigger for a given state, therefore we are implementing a reactive interface for the wrapper.
The callbacks are optional, given that the view might not need to check on each one of the possible states. We take advantage of typescript utility types to implement a partial design pattern.
This makes it so that at least one callback is required since on the contrary, the developer would not want to use them on function, but it is not forcing the developer to use any specific callback.
Seeing results in action
To focus on a specific use case, let’s see this used in an Angular application, which uses RxJx by default.
We can implement an interface for RxJs, to make requests and state management seamless.
The resourceRequestObservable function is just a mapper function to allow any Angular http request observable to be wrapped around an instantaneous loading state, followed by the network response, be it failure, success or an empty object.
The toResource and toFailure functions are pure maps that construct the Resource object accordingly. Since our loading resource is always empty, a constant LOADING object will do.
This way, if our intent is to request a list of “To do” items from the network, we can make the usual request, using the resourceRequestObservable function as a wrapper.
The object subscribed to the event can now react to the multiple outcomes. The onResource map function helps reduce boilerplate, as well as help the developer stick to the designed interface.
Since this case is simple, the component will import the service and manage the results. Once the service is injected and the component is ready to make the request, all that is left is to subscribe to the results.
Without any decision making, the states for the view are updated accordingly. This allows for sleek and readable code, leaving no unexpected outcomes uncovered and reducing intermediate state on the business layer of the application.
Conclusion
Let’s take a step back and see what this code provides:
- It’s a generic pattern. It is reusable no matter the model, state or view.
- It’s reactive. The interface we expose allows for this.
- It’s a framework agnostic solution. It can be adapted to Redux and Vuex the same way we’ve adapted to RxJs.
- It’s lightweight. It is very little code, and, even better, there’s less code to write throughout the application.
- It’s not new. We’re just abstracting code we typically see elsewhere.
However, you may try to use this in different projects and expect different outcomes. It all depends on what new requirements exist, but this is easily iterable and it could still allow for generic use throughout future development.
The idea is pretty straightforward and we can always find a similar interface that fits our application’s needs. If you want to tweak it a little bit, or want to port it to JS, you can give it a shot and make your personal solution based on this concept.
Remember you can check out the code on github, you may find more detail into the code that was implied during the read. The repository also contains a small demo to show how it works on a small Angular application (with a mocked service for simplicity). If you’re convinced this fits your needs you can directly pull the dependency from npm.