The Formula for a Reactive Web Application
We have covered some basic idea and concept about NGRX (state management tool) and how to utilize it in Angular application in my previous blog post. This follow-up post addresses the challenges that arise as web applications grow in complexity and undergo frequent state changes. We will explore how NGRX can be utilized to create a more organized front-end data storage architecture, which serves as an effective solution to these issues.
The issue
You are asked to build a online shopping website, where you should be able to provide a list of products, and a checkout page.
You have a website ready, it looks good, it functions and user can interact with it. But it is not very reactive, as you can see, everytime when the user updates the shopping cart, the total amount does not automatically updates in the cart summary, the user would have to click on Get summary button to be able to get the update-to-date product amount.
Why this happens? Let’s have a break down of what are the data flow here:
- when user land on the page, both cart items and cart summary componets fires the fetch request to get the latest cart item information,
- when user click the + or - button for a product in the cart items component, an update request is fired to update the cart,
- when user clicks on the get summary bnutton, the cart summary component fetches all the product information again and caculate the total amount,
To support the above functionality, CartService is defined like this:
export class CartService {
private baseUrl = 'http://localhost:3000/cart';
constructor(private http: HttpClient) {}
fetchCartItems(): Observable<> {
return this.http.get<>(this.baseUrl);
}
modifyItem(item: any): Promise<any> {
return firstValueFrom(
this.http
.post(this.baseUrl, item, { observe: 'response' })
.pipe(map((r) => r.status))
);
}
}
For detailed source code can be view in here.
To improve the reactiveness of the website, we would like to automatically updates the cart summary whenever the user clicks the + or - on a product. How can we achieve that?
We can uitlize the RXJS library. We can define a subject which will we emits the result of the fetch request, and we subscribe to that subject in the html template to ensure the front-end renders the most up-to-date value. But this won’t solve our problem of auto refreshing the cart summary component when changing the number of the product. One way to solve that is fire a fetch request whenever we finished update request. Here is the updated version for CartService.
export class CartService {
private baseUrl = 'http://localhost:3000/cart';
private cartSubject = new BehaviorSubject([]);
cart$ = this.cartSubject.asObservable().pipe(); // Public observable for components to subscribe to
constructor(private http: HttpClient) {}
fetchCartItems(): Observable<any> {
return this.http.get<>(this.baseUrl).pipe(
tap((items) => {
this.cartSubject.next(items);
}),
switchMap(() => this.cart$.pipe())
);
}
modifyItem(item: any): Promise<any> {
return firstValueFrom(
this.http.post(this.baseUrl, item, { observe: 'response' }).pipe(
switchMap((response) => {
return this.fetchCartItems().pipe(
mapTo(response.status)
);
})
)
);
}
}
For detailed source code can be view in here.
As demostrated in the video, this approach does make the website more reactive, the cart summary changes the value as user updates the product amount. One noticeable disadvantage of this approach is incurrs additional network request.
How to solve that? Maybe we can merge the value from updates with existing value in the subject to generate a new value, in this case, we don’t have to fire a new network request. The updated function for modifyItem is defined as follows.
modifyItem(item: any): Promise<any> {
return firstValueFrom(
this.http.post(this.baseUrl, item, { observe: 'response' }).pipe(
tap(() => {
const updatedItems = this.cartSubject.value.map((orignalItem) => {
if (orignalItem.id === item.id) {
return item;
}
return orignalItem;
});
this.cartSubject.next(updatedItems);
}),
map((r) => r.status)
)
);
}
This solution seems working, we do have a reactive web application without more network request overhead. Happy day. Now your boss is happy with the result, but in the same time, he would like to to make the website more sophisticated to cater to more clients needs, he would like to add more product, and also allow the user to filter the product by the category.
You added the filter functionaility in the backend, and introduced a new fetch by category function in the cartService and updated the template. Hoping everything is fine.
fetchProductsByCategory(category: string): Observable<> {
const queryParam = category ? `?category=${category}` : '';
return this.http.get<>(`${this.baseUrl}${queryParam}`).pipe(
tap((items) => {
this.cartSubject.next(items);
}),
switchMap(() => this.cart$.pipe())
);
}
Ehhhh, the filter logic is working, the website is still reactive, but, it updates the summary component whenever user uses a filter! (The requirement is always to keep the cart summary to have all the product you ordered regardless of the filter.)
Apparently, we need to keep different varaibles to hold the data for different requests, which does not sound scalable at all!
The solution
What would be the best way to solve this problem then? Use subject to keep the record of data change around network boundary is a good start, we just need to ensure the data we stored in the subject is indifferent from the network request parameters, and then we design some mechnism to select the data from subject based on the request filter. In other word, we can have a data store in the front-end that keeps all the latest data from users action (fetch, update, delete), and when user tries to request data, instead of getting the result directly from network request, we serve it from the data store.
-
Dive in the code and show how it works in code.
-
Emphasize the source of the data is the only state you care and everything else is derived from it. User interaction manipulates the source of data and front-end renders just as the logic says.
-
Introduce the concept of local data store, and its value should be capturing data coming from the network boundary. Which means fetch/post/delete/update change the state in the store, and user request data from the store, not from the network, so in this case, store serves as the middle man for serving front-end component demand for data. The middle man request the data on behave of front-end client, and return the data to the front-end client.
-
Shape the concept for binding the resource with a name that the middle man can understand and also binding the name with its API path. (The folder sturcture for datastore collection) And highlight its worth to introduce an object that is front-end based. (Domain specific language).
-
Building a wrapper of data store (In detail)
- how it
selectsthe data to the user - how to dispatch events (can skip dedupe)
- how to reduce data into store
- how it
-
Briefly mention the importance of having the complete coverage of all possible http communication use case,( eg: filter tool, pagination, search, projection). The powerfulness of this structure on controlling the network request. And the extendability on including other client-server communication technologies such as websocket, server side events and etc.
For detailed source code can be view in here.
About testing
-
Throw a question of how normally people write UI test.
- How you write UI test in your company? Set value for a component? query selector? Inspect array of element via their index? You are writing test for your computer, not for testing if a human will see ABC. A good test should be as close to production use case as possible.
- Introduce
Testing library- write your UI test as a real user who uses your application.
-
Introduce how to integrate data store into testing,
- The high level structure of datastore in UI testing
The formula
- Glad you went through the whole blog post, to summarize the all the content above into a simple formula for a reactive web application: use observable pattern + utlizing state management library (eg. NGRX) to store the data coming from the network boundary. (Since all the UI rendering is derived from the data that coming from network boundary.)
References
Why the concept of immutability is so awfully important for a beginner front-end developer?