Angular: Confer the power from your Observables to Signals!
240902-05
The scenario
Let’s see a simple scenario where we want to access data from the back end. In our case, the data consists just of 2 entities: Categories and Items. Each Item belongs to 1 Category, so a Category has several Items. Our goal is to fetch items belonging to a specified category from the back end.
For simplicity, our demo data are provided by 2 JSON files, ‘categories.json’, and ‘items.json’ respectively, and they are located in the src/assets/data subfolder of our Angular project.
Our Angular project has the following Components:
- HeaderComponent
- MenuComponent
- ContentComponent
- FooterComponent
All the components are children-components of the AppComponent (created by default as the bootstrap component), and all the above components are siblings.
The MenuComponent is actually, a menu bar with some buttons. Each button represents a specific category. Categories are fetched from the back end. So, when the user clicks on a button, the items of the specified category are fetched from the back end and displayed in the ContentComponent.
For fetching the data from the back end, we use a data service. This is actually the DataServise that interacts with the back end via the HttpClient and accesses the endpoints of the back-end API, using several methods. Since, all of our components are siblings,
the best practice is to inject the DataService into any “consumer” component. Then, in each “consumer” component we can subscribe to the desired method’s observable, and get the corresponding data.
The initial project using observables and subjects
Find the initial repo of the project here.
Below, we will see the important points of the DataService and the “consumer” components MenuComponent and ContentComponent.
The MenuComponent
As you can see we inject the DataService into the constructor, and then inside the ngOnInit lifehook method we subscribe to the DataService’s ‘getCategories()’ method to obtain the array of Categories, that are used with the menu buttons. Also, when the user clicks on a menu item button then we use the ‘itemSiteMenuClicked()’ click-handling method and we use the DataService’s ‘setCategoryId()’ method to pass the item (the Category object) clicked. Below, find also, the template of the component: menu.component.html:
The DataService
As we’ve said, the DataService is where we use the HttpClient to get data from the back end. We actually use 2 get methods for fetching the Categories and the Items respectively. Both of them return an Observable. The ‘getItems()’ uses also an optional number parameter for the requested category. However, if the categoryId parameter is not provided it returns all the items.
Moreover, we also use a BehaviorSubject (the ‘categoryIdSubject$’) in our DataService, which is used for the category passed in from the MenuComponent. The ‘setCategoryId()’ set’s the ‘next’ value of the categoryIdSubject$ and the getCategoryId() returns it to the caller as an Observable.
All the observables are supported by a common error handler method: the ‘handleError()’ method.
Finally, note that we have also defined 2 interfaces for the Category and the Item entities respectively.
The ContentComponent
In the ContentComponent, we display the items of the specified category. We also inject the DataService into its constructor. For obtaining the changes of the categoryId, we use a Subscription, to the ‘categoryIdSubject$’ BehaviorSubject of the DataSevice. We subscribe inside the ContentComponent constructor, and when the subscription returns a categoryId larger than 0, we call the ‘getCategoryItems()’ method, which in turn, subscribes to the DataService ‘getItems()’ method passing the categoryId obtained before, and then gets the returned items. Of course, we also destroy the subscription upon ContentComponent termination. Below, find also, the template of the component: content.component.html:
That’s it with the well-known usage of observables. Find the so-far repo here.
Now, let’s see how we can transform our code using signals.
Using Signals
There are many different implementations of signals, but our approach here is based on using an intermediate service that will be are one-stop store for signals we will be using. or our case we will name our new service: ‘content’. You can create it by using the following Angular CLI command:
ng g s content --skip-tests=true
ContentServise
We actually, will use one signal for each piece of information (e.g. an entity) that needs to be exchanged between components. Generally, each signal should be supported by a corresponding method that is responsible for dealing with the DataService.
In our case, we want just 2 signals, one for categories and one for items, i.e.:
public $categories = signal< ICategory[]>([]);
public $items = signal< Iitem[]>([]);
As you might have noticed a signal (like a BehaviorSubject) requires an initial value to be set. So initially we use empty arrays.
The supporting method for the $categories signal can be made like this:
public signalCategories(): void {
this.dataService.getCategories().subscribe((categories: ICategory[]) => {
this.$categories.set(categories);
});
}
It simply subscribes to the corresponding method of the DataService and upon data is fetched, assigns it to the $categories signal.
Similarly, we can make the supporting method for the $items signal.
An important point here is the following: The initial value of the $categories signal is an empty array. Moreover, the “consumer” component MenuComponent requires very early to access the categories, because they are the values of the menu items are displayed.
But who is going to initially call the signalCategories() supporting method to set the $categories signal with real data? Well, the method can be called from the “consumer” which is interested in this, but here we will do so from the ContentService constructor.
That said, the full code of the ContentService becomes:
The MenuComponent
Let’s now see how we will update the MenuComponent and its menu items (categories)
We have to remove the DataService, and the respective subscription(s) to any observable. Then, we just need to inject the ContentService and use the effect() method block into the constructor to bring the value of the $categories signal and assign it to the menu items. Finally, we have to update the ‘itemSiteMenuClicked()’ method. We actually, have to call the ContentService ‘signalItems()’ passing it the clicked menu item (the category selected). (Note, that this results in updating the $items signal, which in turn can be used to update the items into the ContentComponent as we will see a bit later on). So, the MenuComponent becomes:
The ContentComponent
Similarly, we can work updating the ContentComponent, by first, removing the DataService and all related observables and subscriptions, etc, Then we can use the effect() method block into the constructor to bring the value of the $items signal and assign it to the items. The ContentComponent becomes:
A minor update is also necessary to the template of the ComponentComponent since we don’t use any more a categoryId, and the related subscription. The content.component.html becomes:
So that’s it. The result remains identical to the output we have seen in the beginning.
Find the final repo here:
Conclusions
With the approach we’ve seen before, we confer the power from observables to signals, using an intermediate service.
Using such a service might look like adding one more level of complexity, but it actually has a lot of benefits:
- It allows us to use just signals with any “consumer” component.
- It frees us from adding observable boilerplate into each separate component and managing any subscriptions. (Signals are self-destroyed).
- It gathers together in one place all the necessary observables’ subscriptions (due to HttpClient data access logic). This offers us a good maintainability of this code.
- Allows us to add more process logic -if necessary- into the intermediate service and transform the data information received into signals.
- Finally, we provide Signals instead of Observables to any component, that is interested in. Thus, the code of the “consumer” components is kept quite tidy and clean.
Signals becomes the new standard and our Angular code should sooner or later be adapted to this.
That’s it! Thank you for reading!
Happy Coding!