DEV Community

loading...
Cover image for Reactive Angular components with presenters - Part 2

Reactive Angular components with presenters - Part 2

Mathias Remshardt
Full-stack-developer, Learner
・9 min read

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

Example app components

  • UsersRestAdapter: Interface for requesting the user list from the server.
  • ProductionUserRestAdapter: Service implementation of UserRestAdapter.
    • Using an interface has been done on purpose as it allows for "mocking" the adapter for depending services/components like UsersService.
  • 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 the UsersWithPresenterComponent.
  • 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:

  1. Tests targeted at the ui (template required) --> slower
  2. 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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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).

Discussion (0)

Forem Open with the Forem app