Reactive Angular components with presenters - Part 2
Mathias Remshardt
Posted on January 9, 2021
In the first part of the article we looked at the issue of slow running (component) unit tests in one of our projects. After having discussed different approaches for improving the execution time, moving business logic out of the affected components has been chosen as the preferred solution. Based on the derived requirements for the (new) component structure, the main ideas of a Flutter BLoc and Mode-View-Presenter inspired component structure have been explained.
In contrast to the more theoretical discussion in part one, part two focuses on showing the approach in practice by means of a simple example application. This will then enable us to assess the new found component structure in regard to the requirements formulated in part one.
Example application
As it is often the case with these (simpler) applications it cannot showcase all elements and subtleties found in real projects without loosing focus on the main idea. However it should serve a good overview for how a presenter based component implementation can look like.
The main feature of the example application is to show a list of user names. In case of interest, a running version can be seen here The component has been implemented twice which allows for a direct comparison of two variants:
- The first variant contains all the "logic" in the component controller, reflecting our "old" approach
- For the second variant the logic is taken care of by a presenter, reflecting the "new" approach
Next, a quick overview and explanation of the relevant parts is given. In case you prefer reading code to text you can find it here and directly jump to the results section.
The rest of us will start with the "Architecture" overview.
Architecture
-
UsersRestAdapter
:Interface
for requesting the user list from the server. -
ProductionUserRestAdapter
:Service
implementation ofUserRestAdapter
.- Using an interface has been done on purpose as it allows for "mocking" the adapter for depending services/components like
UsersService
.
- Using an interface has been done on purpose as it allows for "mocking" the adapter for depending services/components like
-
UsersService
: Manages/encapsulates the global user state as an Angular service.-
list
all users -
refresh
the list of users
-
-
UsersComponent
: Shows the list of usernames. -
UsersPresenter
:Service
managing the state of theUsersWithPresenterComponent
. -
UsersWithPresenterComponent
: Shows the list of usernames using an presenter for component logic and state management. -
UsersCounterComponent
: Shows the number of users.- This has been put into a dedicated component on purpose as it shows how a presenter can be used for sharing overarching state and thus avoiding prop drilling
- Other files not relevant for the discussion itself.
As described UsersComponent
and UsersWithPresenterComponent
, both implement the same user interface and features to enable a direct comparison.
Elements
The section will give some implementation details for the elements relevant for the discussion in this article.
Classes/files not important for the approach are not covered.
We will also define the required test categories for each discussed component/service, as testing, especially test performance, plays an important role in this article.
As a quick reminder the two categories are:
- Tests targeted at the ui (template required) --> slower
- Test targeted at business logic in the component (no template required) --> faster
UsersComponent
The UsersComponent
uses the Angular Material UI
library to display a simple list of users:
@Component({
selector: 'app-users',
templateUrl: './component.html',
styleUrls: ['./component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UsersComponent implements OnInit {
readonly userNames$: Observable<ReadonlyArray<string>>;
readonly userCount$: Observable<number>;
constructor(private usersService: UsersService) {
this.userNames$ = this.setupUsers();
this.userCount$ = this.setupUserCount();
}
ngOnInit(): void {
this.usersService.refreshUsers();
}
private setupUsers(): Observable<ReadonlyArray<string>> {
return this.usersService.users$.pipe(extractUserNames());
}
private setupUserCount(): Observable<number> {
return this.usersService.users$.pipe(extractNumberOfUsers());
}
}
As mentioned in Architecture
the main functionality of the component is to display a list of usernames.
The list is created by subscribing to the users$
Observable in the global UsersService
. As the component is only interested in the list of names, it creates a new Observable by mapping
over the global users$
list to extract the usernames from the User
objects (done by the setupUsers
method called in the constructor).
The userCount$
property uses the same approach for extracting the number of users.
For the sake of simplicity, a refresh of the global users list is triggered once the component gets initialized. This ensures that users are available in the UsersService
.
The associated component template subscribes to the list by employing the build-in async
pipe. Subsequently, it iterates over the usernames and displays each in a material-list
/material-list-item
.
The user count is displayed by simply subscribing to the userCount$
property.
<ng-container *ngIf="userNames$ | async as userNames">
<mat-list>
<h3 mat-subheader>List</h3>
<mat-list-item class="userNames__element" *ngFor="let userName of userNames"
>{{userName}}</mat-list-item
>
<h3 mat-subheader>Count</h3>
<mat-list-item class="userNames__count"
>Number of Users: {{userCount$ | async}}</mat-list-item
>
</mat-list>
</ng-container>
Tests
As ui and business logic concerns are mixed in the component, both test categories are represented. This is exactly the type of component which has been deemed problematic for our project as it performs template compilation for both test categories.
UsersWithPresenterComponent
@Component({
selector: 'app-users-presenter',
templateUrl: './component.html',
styleUrls: ['./component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [UsersPresenter],
})
export class UsersWithPresenterComponent {
constructor(public presenter: UsersPresenter) {}
}
The functionality is equivalent to the one in UserComponent
. The main difference is that all the implementation required for loading and converting the list of users has been moved to the UsersPresenter
. By adding the latter to the list of component providers
and making it part of the constructor, the template can directly subscribe to the public userNames$
property in the presenter.
As mentioned in the beginning, a dedicated component UserCount
is leveraged to display the number of users. Although this would not necessarily be required in a "real" application (due to the low complexity), it shows how prop drilling can be avoided by injecting the presenter in deeper levels of the component tree.
<ng-container *ngIf="presenter.userNames$ | async as userNames">
<mat-list>
<h3 mat-subheader>List</h3>
<mat-list-item class="userNames__element" *ngFor="let userName of userNames"
>{{userName}}</mat-list-item
>
<h3 mat-subheader>Count</h3>
<mat-list-item> <app-user-counter></app-user-counter></mat-list-item>
</mat-list>
</ng-container>
Tests
Having most of the logic now extracted to the UsersPresenter
leaves only template related functionalities in the component itself. As a consequence, all category two tests can be covered in the presenter tests and template compilation is only performed for ui tests (category one) where it is indeed required.
This is exactly what we wanted to achieve with the new structure in regard to testing.
UsersPresenter
@Injectable()
export class UsersPresenter {
readonly userNames$: Observable<ReadonlyArray<string>>;
readonly userCount$: Observable<number>;
constructor(private usersService: UsersService) {
this.userNames$ = this.setupUserNames();
this.userCount$ = this.setupUserCount();
this.onInit();
}
private setupUserNames(): Observable<ReadonlyArray<string>> {
return this.usersService.users$.pipe(extractUserNames());
}
private setupUserCount(): Observable<number> {
return this.usersService.users$.pipe(extractNumberOfUsers());
}
private onInit(): void {
this.usersService.refreshUsers();
}
}
The UsersPresenter
encapsulates the implementation logic that has been extracted from UsersWithPresenterComponent
. It makes the list of users accessible to the component via the public userNames$
property (in the same way as UsersComponent
where it is located in the component controller itself).
The UsersPresenter
already gives an impression how global state (users list) can be declaratively processes/combined with local state when both use the same underlaying, reactive foundation (RxJs
in our case). With NgRx
, as another example, a selector would be used instead of directly accessing the users$
property in UsersService
.
Tests
As the presenter is a service it only contains category two tests.
UserCountComponent
@Component({
selector: 'app-user-counter',
templateUrl: './component.html',
styleUrls: ['./component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCounterComponent {
constructor(public presenter: UsersPresenter) {}
}
The UserCountComponent
can leverage the UsersPresenter
to display the number of users. This showcases how presenters, injected deeper in the component tree, can be an alternative to using @Input
properties for passing data.
The UserPresenter
is available here, as UserCounterComponent
is a child node in the template of UsersComponent
. Worth mentioning may be, that it does not have to be a direct child.
Tests
The component contains no business logic and therefor only category one tests are applicable.
Results
With the example application implemented it is now possible to see if component presenters can actually help to:
- decrease unit test time for components and component related business logic
- improve components and components structure
- share common logic/state in case appropriate
Unit test time
All implementations that have been located in the component and factored out can now be tested in isolation. This reduces the number of tests performing template compilation leading to a reduced test time.
At first glance it does look like a lot of effort for a few ms
e.g. for the should emit list of user names
test in the captured test run. However, these small improvements in run time do add up when the test suite size increases.
So the decreased test run time looks (even if only a few ms
) promising. It should be mentioned though, that the effect may be lower when the complexity of the test itself increases, reducing the "impact" of template compilation.
The complete html report of the test run can be found in the test-reports
folder.
For our (real) project we could not make a direct before/after comparison as the migration is still ongoing. We are doing a kind of "on-touch" refactoring (instead of a big bang) to strike the balance between new features and code improvements. Nevertheless, we did make some measurements for our more complex components and saw improvements in test time.
For the project (in contrast to the example application) the component tests have been removed all together, so only the ones for the presenters are left. After maintaining the former for some time we did not see any additional benefits as the template part is tested by e2e tests. This is/was just our experience so your mileage may vary here.
Lightweight, modularized and encapsulated components
Even though the UsersComponent
and UsersWithPresenterComponent
are of low complexity, the simple example already shows the improvements of separating the "behind-the-scenes" implementation to a dedicated presenter. Not only does this lead to a component with almost no additional code (besides what is required for the Angular framework). It also separates the ui/template related implementations from the more involved state handling/orchestration concerns.
Based on our experience so far, we formulated three structural elements for our projects:
- Implementations e.g. global services, RxJs... for global state
- Presenters for component state and/or business logic (implemented as services provided by the component)
- Components concerned with the user interface
These three building blocks not only help us to make our components simpler (in case required multiple component presenters are used). We also made good experiences when introducing new team members as the three categories are a guideline where an implementation should be located.
Sharing business logic and state
Although somehow artificial (as hard to do otherwise in a simple example) the added UsersCount
component shows how a presenter provided at a higher level in the component tree can be shared/reused at a lower level. One can probably imagine how this can be applied to avoid e.g. prop drilling when the tree height increases.
For our project, prop drilling and duplicated component state/business logic was/is not really an issue as:
- we heavily make us of generic components which take configurations as
@Input
properties and therefor manage state independently - shared business logic was and is factored into pure and shared plain old JavaScript functions
- global state and business logic is covered by
NgRx
Bonus - Change Detection
In the beginning of the article it has been mentioned, that presenters can be beneficial for change detection when completely based on Observables.
This is not necessarily required but opens up the possibility to enable the onPush
change detection strategy for additional performance benefits.
Conclusion
...for the pattern
Time for a recap:
We started the journey with the problem of slow running unit tests and looking for solutions potentially reducing the execution time. Moving non template related functionalities out of the component came out as our favorite option. It also opened up an opportunity to improve our component structure leading to additional requirements.
After some theory about the patterns inspiring the new found approach, we looked at a simple example application implementing the same component feature (displaying a list of users) twice. This allowed a before/after comparison not possible in our real project for practical reasons.
As a final result the newly employed pattern could be shown as beneficial for our requirements:
- lightweight, modularized and encapsulated components
- sharing of local state and business logic
- unit test time
At the end one could state that our initial problem (unit test times) was more solved as a side effect of the newly imposed presenter based structure.
for the Project
In our project we have (up to this point) made good experiences, both for new and refactored components.
We are using NgRx
for global state- and (now) presenters for local state management. As both NgRx
as well as our implementations of presenters are completely based on Observables, the global and local state can be combined or piped
pretty easily.
What we really like about it is the clear structure it provides in combination with simplified components and testing. It does require some learning and "getting used to" due to being completely based on Observables.
However, we do not consider this a drawback. The Angular framework and libraries are already heavily relying on Observables (e.g. when looking at the HttpClient
, the RoutingModule
or libraries like NgRx
), so learning their usage is kind of a requirement. And almost every framework, library... needs some time and effort to become proficient in it.
NgRx component
Why has @ngrx/component not been considered?.
The simple reason is, that it was not yet ready/available.
Otherwise, as we are using NgRx
, it would have been a compelling alternative as it gives similar advantages in regard to testing and component/application structure with additional benefits like component based selectors
.
We will definitely consider it for future projects where NgRx
is employed.
The availability of @ngrx/component
does, in my opinion, not make the here described approach superfluous. Not all projects use NgRx
so in case an approach only based on Angular primitives (Providers
, Services
and Observables
) is needed, the MVP pattern and especially presenters can be an option with similar benefits (depending on how it is implemented).
Posted on January 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 8, 2024
September 10, 2024